diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da1299b035..eaea9a841d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ jobs: # https://github.com/actions/checkout/issues/881 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/build_enterprise.yml b/.github/workflows/build_enterprise.yml index bccdb84516..9c4c8cec8f 100644 --- a/.github/workflows/build_enterprise.yml +++ b/.github/workflows/build_enterprise.yml @@ -39,7 +39,7 @@ jobs: - name: Clone submodules run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/fork-pr-notice.yml b/.github/workflows/fork-pr-notice.yml index 8c06c708d5..79d33d3b6d 100644 --- a/.github/workflows/fork-pr-notice.yml +++ b/.github/workflows/fork-pr-notice.yml @@ -15,7 +15,7 @@ jobs: if: github.event.pull_request.base.repo.full_name != github.event.pull_request.head.repo.full_name steps: - name: Add auto-generated commit warning - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | github.rest.issues.createComment({ diff --git a/.github/workflows/generate_github_pages.yml b/.github/workflows/generate_github_pages.yml index 5fddd72eea..a4286491cb 100644 --- a/.github/workflows/generate_github_pages.yml +++ b/.github/workflows/generate_github_pages.yml @@ -14,7 +14,7 @@ jobs: - name: ⏬ Checkout with LFS uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3 - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -23,7 +23,7 @@ jobs: with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Set up Python 3.12 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.13 - name: Run World screenshots generation script diff --git a/.github/workflows/gradle-wrapper-update.yml b/.github/workflows/gradle-wrapper-update.yml index a9cb463629..f826bb8205 100644 --- a/.github/workflows/gradle-wrapper-update.yml +++ b/.github/workflows/gradle-wrapper-update.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: - uses: actions/checkout@v5 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 name: Use JDK 21 if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch' with: diff --git a/.github/workflows/maestro-local.yml b/.github/workflows/maestro-local.yml index b784514d13..64eff695d9 100644 --- a/.github/workflows/maestro-local.yml +++ b/.github/workflows/maestro-local.yml @@ -28,7 +28,7 @@ jobs: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 ref: ${{ github.ref }} - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 name: Use JDK 21 with: distribution: 'temurin' # See 'Supported distributions' for available options diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f09e36b785..b494d9fe8c 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index 177b429a10..f89373b44f 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -21,7 +21,7 @@ jobs: uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3 - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -62,7 +62,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index 56d40d0cf6..a5c2504f8e 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Trigger pipeline - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ secrets.ENTERPRISE_ACTIONS_TOKEN }} script: | diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 929bcb5dcd..6063fe0099 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -15,7 +15,7 @@ jobs: pull-requests: read steps: - name: Add notice - uses: actions/github-script@v7 + uses: actions/github-script@v8 if: contains(github.event.pull_request.labels.*.name, 'X-Blocked') with: script: | @@ -39,7 +39,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN_READ_ORG }} - name: Add label if: steps.teams.outputs.isTeamMember == 'false' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | github.rest.issues.addLabels({ @@ -58,7 +58,7 @@ jobs: github.event.pull_request.head.repo.full_name != github.repository steps: - name: Close pull request - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | github.rest.issues.createComment({ diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 1157a02eea..6e9dea69ab 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python 3.12 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.13 - name: Search for invalid screenshot files @@ -47,7 +47,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -56,7 +56,7 @@ jobs: with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Set up Python 3.12 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.13 - name: Search for invalid dependencies @@ -85,7 +85,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -125,7 +125,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -169,7 +169,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -209,7 +209,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -249,7 +249,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index a5172188ee..a4d54afeb4 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -34,7 +34,7 @@ jobs: with: persist-credentials: false - name: ☕️ Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c3f51f8ec4..661110338d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -61,7 +61,7 @@ jobs: - name: Clone submodules run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -89,7 +89,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index d85e4edf27..080fcef0e0 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -28,7 +28,7 @@ jobs: # https://github.com/actions/checkout/issues/881 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index 44c3a24eb7..b5548fd2b1 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -22,7 +22,7 @@ jobs: with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Set up Python 3.12 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.13 - name: Setup Localazy diff --git a/.github/workflows/sync-sas-strings.yml b/.github/workflows/sync-sas-strings.yml index c6c6a76c9f..84b0cb521c 100644 --- a/.github/workflows/sync-sas-strings.yml +++ b/.github/workflows/sync-sas-strings.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python 3.12 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.13 - name: Install Prerequisite dependencies diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a0ffe74f6f..55f27aa9ba 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,7 +47,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: ☕️ Use JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -82,7 +82,7 @@ jobs: # https://github.com/codecov/codecov-action - name: ☂️ Upload coverage reports to codecov - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index e3fcb2ef13..e1b1c5c8ab 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ captures/ # Android Studio 3 in .gitignore file. .idea/caches .idea/copilot +.idea/copilot.* .idea/inspectionProfiles # Shelved changes in the IDE .idea/shelf diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 254a1fc3cc..3efb2d8dd4 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.maestro/tests/account/verifySession.yaml b/.maestro/tests/account/verifySession.yaml index f1f4552709..a16322543f 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: "Verification complete" + visible: "Device verified" timeout: 30000 - tapOn: "Continue" diff --git a/CHANGES.md b/CHANGES.md index 3f6a82a468..e4d138bfd5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,200 @@ +Changes in Element X v25.09.2 +============================= + +## What's Changed +### ✨ Features +* Show progress dialog while we are sending invites in a room by @richvdh in https://github.com/element-hq/element-x-android/pull/5342 +* Call: RTC decline event support by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/5305 +* Add room info to the thread's top app bar by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5374 +### 🙌 Improvements +* Use the new RtcNotification event instead of the now deprecated CallNotify by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/5357 +### 🐛 Bugfixes +* Increase Element Call audio init delay ensuring the right audio device is used by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5315 +* Do not center the dialog title text for dialogs with no icon by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5332 +* Media viewer: release the `ExoPlayers` when the hosting composables are disposed by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5351 +* Make PushData.clientSecret mandatory. by @bmarty in https://github.com/element-hq/element-x-android/pull/5369 +* Cleanup ftue code and ensure verification confirmation is displayed by @bmarty in https://github.com/element-hq/element-x-android/pull/5379 +* Change in clear cache behavior by @bmarty in https://github.com/element-hq/element-x-android/pull/5388 +* fix (room navigation) : fix navigation when leaving room/space by @ganfra in https://github.com/element-hq/element-x-android/pull/5376 +* fix (timeline) : forward pagination regression by @ganfra in https://github.com/element-hq/element-x-android/pull/5389 +* When joining a call, wait for the `content_loaded` action by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5399 +* Ensure the thread summary sender's display name won't wrap to the next line by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5403 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5349 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5385 +### 🧱 Build +* Improve release script and the file Versions.kt by @bmarty in https://github.com/element-hq/element-x-android/pull/5318 +* Dependency: extract the Matrix SDK and add instructions for upgrading the library by @bmarty in https://github.com/element-hq/element-x-android/pull/5363 +* Add test on DefaultSpaceEntryPoint by @bmarty in https://github.com/element-hq/element-x-android/pull/5343 +### 🚧 In development 🚧 +* Space list by @bmarty in https://github.com/element-hq/element-x-android/pull/5320 +* Feature : Join Space (WIP) by @ganfra in https://github.com/element-hq/element-x-android/pull/5378 +### Dependency upgrades +* Update activity to v1.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5324 +* Update dependency com.google.truth:truth to v1.4.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5322 +* Update dependency io.sentry:sentry-android to v8.21.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5310 +* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5323 +* Update dependency androidx.sqlite:sqlite-ktx to v2.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5337 +* Update camera to v1.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5336 +* Update dependency com.posthog:posthog-android to v3.21.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5333 +* Update dependency com.google.testparameterinjector:test-parameter-injector to v1.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5341 +* Upgrade Rust SDK bindings to v25.09.15 by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5353 +* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.16 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5359 +* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.18 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5365 +* Update telephoto to v0.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5350 +* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5377 +* Update dependency com.google.firebase:firebase-bom to v34.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5367 +* Upgrade Element Call embedded dependency to `v0.16.0-rc.4` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5391 +* Update dependencyAnalysis to v3 (major) by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5194 +* Update dependency org.maplibre.gl:android-sdk to v11.13.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5381 +* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5396 +* Update plugin dependencycheck to v12.1.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5382 +* Update dependency io.sentry:sentry-android to v8.22.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5397 +### Others +* Cleanup nodes by @bmarty in https://github.com/element-hq/element-x-android/pull/5358 +* Complete test on MediaGalleryPresenter by @bmarty in https://github.com/element-hq/element-x-android/pull/5361 +* Remove dead code by @bmarty in https://github.com/element-hq/element-x-android/pull/5306 +* Introduce BugReportFlowNode, and remove NavTarget.ViewLogs from RootFlowNode by @bmarty in https://github.com/element-hq/element-x-android/pull/5370 +* When logging out from Pin code screen, logout from all the sessions. by @bmarty in https://github.com/element-hq/element-x-android/pull/5372 +* Clean MatrixAuthenticationService and SessionStore API by @bmarty in https://github.com/element-hq/element-x-android/pull/5371 +* Add logs to detect duplicates in the room list by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5364 +* Add troubleshoot notification test about blocked users by @bmarty in https://github.com/element-hq/element-x-android/pull/5394 +* Add thread decoration with latest event details by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5355 +* Rework on messages view top bars by @bmarty in https://github.com/element-hq/element-x-android/pull/5401 +* Put developer settings at the end of the view by @p1gp1g in https://github.com/element-hq/element-x-android/pull/5387 + +## New Contributors +* @p1gp1g made their first contribution in https://github.com/element-hq/element-x-android/pull/5387 + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.09.1...v25.09.2 + +Changes in Element X v25.09.1 +============================= + +## What's Changed + +We have migrated our DI libraries from Dagger and Anvil to Metro. If you need more details on the migration steps, please read the [documentation](https://github.com/element-hq/element-x-android/blob/develop/docs/migration_to_metro.md). + +### ✨ Features +* Allow replying to a message with an attachment by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5261 +* Add emoji search to the reaction emoji picker by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5255 +### 🙌 Improvements +* Spelling correction in Update FeatureFlags.kt by @escix in https://github.com/element-hq/element-x-android/pull/5232 +* [a11y] Add content descriptions to room list item indicators by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5236 +* [a11y] Add click action to the message bottom sheet handle by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5228 +### 🐛 Bugfixes +* Reload member list after moderation actions by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5268 +* Restore view log code by @bmarty in https://github.com/element-hq/element-x-android/pull/5294 +* Detect mime type when picking a file by @bmarty in https://github.com/element-hq/element-x-android/pull/5291 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5249 +* Sync Strings - new translations to Korean by @ElementBot in https://github.com/element-hq/element-x-android/pull/5286 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5290 +### 🧱 Build +* Iterate on build chain by @bmarty in https://github.com/element-hq/element-x-android/pull/5272 +* Cleanup our DI solution and add documentation about the migration to Metro by @bmarty in https://github.com/element-hq/element-x-android/pull/5287 +* Revert agp to 8.11 by @bmarty in https://github.com/element-hq/element-x-android/pull/5311 +### 🚧 In development 🚧 +* Space: add content in home screen by @bmarty in https://github.com/element-hq/element-x-android/pull/5273 +* Hide the home navigation bar if the user is not a member of any Space. by @bmarty in https://github.com/element-hq/element-x-android/pull/5292 +### Dependency upgrades +* Update dependency org.maplibre.gl:android-sdk to v11.13.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5239 +* Update dependency com.google.firebase:firebase-bom to v34.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5245 +* Update dependency com.posthog:posthog-android to v3.21.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5238 +* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5251 +* Update plugin sonarqube to v6.3.1.5724 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5235 +* Update android.gradle.plugin to v8.12.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5244 +* Update dependency io.element.android:emojibase-bindings to v1.4.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5250 +* Update actions/setup-python action to v6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5270 +* Update dependency com.posthog:posthog-android to v3.21.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5275 +* Migrate Anvil KSP to Metro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5253 +* Update actions/github-script action to v8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5284 +* Update codecov/codecov-action action to v5.5.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5274 +* Update dependency io.sentry:sentry-android to v8.21.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5293 +### Others +* Remove LoginUserStory. by @bmarty in https://github.com/element-hq/element-x-android/pull/5237 +* Update state in runUpdatingState when CancellationException occurs by @jbrenorv in https://github.com/element-hq/element-x-android/pull/5243 +* Refactor: Move InMemorySessionStore to test module by @bmarty in https://github.com/element-hq/element-x-android/pull/5252 +* Enable `largeHeap` option to have a larger max heap size by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5258 +* Set a custom request config for the Client by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5266 +* Set shortcut ID on received notifications to make them appear as a Conversation by @frebib in https://github.com/element-hq/element-x-android/pull/5192 +* Improve management of shortcut ids. by @bmarty in https://github.com/element-hq/element-x-android/pull/5303 + +## New Contributors +* @escix made their first contribution in https://github.com/element-hq/element-x-android/pull/5232 +* @jbrenorv made their first contribution in https://github.com/element-hq/element-x-android/pull/5243 + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.09.0...v25.09.1 + +Changes in Element X v25.09.0 +============================= + +This release is the same as `25.08.4` but it includes performance fixes for the timeline load times, included in the Rust SDK version upgrade and internal changes for Element Call. + +## What's Changed +### 🧱 Build +* Revert "Try following KSP incremental best practices on `anvilcodegen`" by @bmarty in https://github.com/element-hq/element-x-android/pull/5233 +### Dependency upgrades +* Update dependency io.element.android:element-call-embedded to v0.15.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5229 +* Update dependency org.matrix.rustcomponents:sdk-android to v25.8.26 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5230 +* Downgrade sonar scanner gradle plugin to `v6.2.0.5505` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5234 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.08.4...v25.09.0 + +Changes in Element X v25.08.4 +============================= + +## What's Changed +### ✨ Features +* Threads - first iteration by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5165 +* Add shortcut suggestions for rooms, remove then when leaving by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5180 +* Allow replying to any remote message in a thread by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5201 +### 🙌 Improvements +* Create room flow rework by @bmarty in https://github.com/element-hq/element-x-android/pull/5166 +### 🐛 Bugfixes +* Fix bitrate value used for video transcoding by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5183 +* Fix sending videos in Android 11 and lower by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5186 +* Ensure that only one DataStore is active for the same file. by @bmarty in https://github.com/element-hq/element-x-android/pull/5198 +* Handle preference stores corruption by clearing them by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5086 +* Use variable bitrate mode when transcoding to ensure compatibility with old devices by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5223 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5178 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5211 +### 🧱 Build +* Build release with the latest build tools 36.0.0 by @bmarty in https://github.com/element-hq/element-x-android/pull/5173 +* Try following KSP incremental best practices on `anvilcodegen` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5205 +* Split deeplink module and remove setupAnvil from api modules by @bmarty in https://github.com/element-hq/element-x-android/pull/5210 +* Introduce a11y screenshot test by @bmarty in https://github.com/element-hq/element-x-android/pull/5214 +* Custom logo on on boarding screen. by @bmarty in https://github.com/element-hq/element-x-android/pull/5217 +### 🚧 In development 🚧 +* Space UI component by @bmarty in https://github.com/element-hq/element-x-android/pull/5197 +* Add UI components for spaces. by @bmarty in https://github.com/element-hq/element-x-android/pull/5207 +### Dependency upgrades +* Update core to v1.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5168 +* Update kotlin by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5169 +* Update dependency org.matrix.rustcomponents:sdk-android to v25.8.18 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5182 +* Update android.gradle.plugin to v8.12.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5184 +* Update dagger to v2.57.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5193 +* Update actions/setup-java action to v5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5196 +* Update codecov/codecov-action action to v5.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5191 +* Update plugin ktlint to v13.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5204 +* Update dependency com.posthog:posthog-android to v3.20.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5206 +* Update dependency org.jsoup:jsoup to v1.21.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5212 +* Update dependency com.posthog:posthog-android to v3.20.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5213 +* Update plugin sonarqube to v6.3.0.5676 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5220 +* Update dependency io.sentry:sentry-android to v8.20.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5216 +* Update dependency org.matrix.rustcomponents:sdk-android to v25.8.25 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5219 +### Others +* Iterate on invite people UI by @bmarty in https://github.com/element-hq/element-x-android/pull/5185 +* AnalyticsOptInStateProvider does not need to have an injected constructor by @bmarty in https://github.com/element-hq/element-x-android/pull/5215 +* Add extra logs for sending media by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5218 +* Rename custom_logo to onboarding_logo by @bmarty in https://github.com/element-hq/element-x-android/pull/5226 +* Add unit test on VideoCompressorHelper by @bmarty in https://github.com/element-hq/element-x-android/pull/5227 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.08.3...v25.08.4 + Changes in Element X v25.08.3 ============================= diff --git a/anvilannotations/.gitignore b/annotations/.gitignore similarity index 100% rename from anvilannotations/.gitignore rename to annotations/.gitignore diff --git a/anvilannotations/build.gradle.kts b/annotations/build.gradle.kts similarity index 87% rename from anvilannotations/build.gradle.kts rename to annotations/build.gradle.kts index 66b88b4dfa..00d0735292 100644 --- a/anvilannotations/build.gradle.kts +++ b/annotations/build.gradle.kts @@ -8,7 +8,3 @@ plugins { alias(libs.plugins.kotlin.jvm) id("com.android.lint") } - -dependencies { - api(libs.inject) -} diff --git a/anvilannotations/src/main/kotlin/io/element/android/anvilannotations/ContributesNode.kt b/annotations/src/main/kotlin/io/element/android/annotations/ContributesNode.kt similarity index 91% rename from anvilannotations/src/main/kotlin/io/element/android/anvilannotations/ContributesNode.kt rename to annotations/src/main/kotlin/io/element/android/annotations/ContributesNode.kt index d91a4761fc..06c5749736 100644 --- a/anvilannotations/src/main/kotlin/io/element/android/anvilannotations/ContributesNode.kt +++ b/annotations/src/main/kotlin/io/element/android/annotations/ContributesNode.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.anvilannotations +package io.element.android.annotations import kotlin.reflect.KClass @@ -13,7 +13,7 @@ import kotlin.reflect.KClass * Adds Node to the specified component graph. * Equivalent to the following declaration: * - * @Module + * @BindingContainer * @ContributesTo(Scope::class) * abstract class YourNodeModule { diff --git a/anvilcodegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/anvilcodegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider deleted file mode 100644 index d5c111b0b2..0000000000 --- a/anvilcodegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider +++ /dev/null @@ -1 +0,0 @@ -io.element.android.anvilcodegen.ContributesNodeProcessorProvider diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ea45e51bf8..9be152e9e7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,7 +13,6 @@ 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 import extension.GitRevisionValueSource import extension.allEnterpriseImpl @@ -23,8 +22,9 @@ import extension.allServicesImpl import extension.buildConfigFieldStr import extension.koverDependencies import extension.locales -import extension.setupAnvil +import extension.setupDependencyInjection import extension.setupKover +import extension.testCommonDependencies import java.util.Locale plugins { @@ -37,7 +37,7 @@ plugins { alias(libs.plugins.licensee) alias(libs.plugins.kotlin.serialization) // To be able to update the firebase.xml files, uncomment and build the project - // id("com.google.gms.google-services") + // alias(libs.plugins.gms.google.services) } setupKover() @@ -103,7 +103,8 @@ android { } val baseAppName = BuildTimeConfig.APPLICATION_NAME - logger.warnInBox("Building ${defaultConfig.applicationId} ($baseAppName)") + val buildType = if (isEnterpriseBuild) "Enterprise" else "FOSS" + logger.warnInBox("Building ${defaultConfig.applicationId} ($baseAppName) [$buildType]") buildTypes { val oidcRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android" @@ -247,11 +248,7 @@ knit { } } -setupAnvil( - generateDaggerCode = true, - generateDaggerFactoriesUsingAnvil = false, - componentMergingStrategy = ComponentMergingStrategy.KSP, -) +setupDependencyInjection() dependencies { allLibrariesImpl() @@ -260,6 +257,7 @@ dependencies { allEnterpriseImpl(project) implementation(projects.appicon.enterprise) } else { + implementation(projects.features.enterprise.implFoss) implementation(projects.appicon.element) } allFeaturesImpl(project) @@ -293,12 +291,7 @@ dependencies { implementation(libs.matrix.emojibase.bindings) - testImplementation(libs.test.junit) - testImplementation(libs.test.robolectric) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.toolbox.test) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 34a8fe31f5..26e8c7dc23 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:localeConfig="@xml/locales_config" android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" 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 a4bfe0c60d..a075929552 100644 --- a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt @@ -9,16 +9,16 @@ package io.element.android.x import android.app.Application import androidx.startup.AppInitializer +import dev.zacsweers.metro.createGraphFactory 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.libraries.di.DependencyInjectionGraphOwner +import io.element.android.x.di.AppGraph import io.element.android.x.info.logApplicationInfo import io.element.android.x.initializer.CrashInitializer import io.element.android.x.initializer.PlatformInitializer -class ElementXApplication : Application(), DaggerComponentOwner { - override val daggerComponent: AppComponent = DaggerAppComponent.factory().create(this) +class ElementXApplication : Application(), DependencyInjectionGraphOwner { + override val graph: AppGraph = createGraphFactory().create(this) override fun onCreate() { super.onCreate() diff --git a/app/src/main/kotlin/io/element/android/x/MainNode.kt b/app/src/main/kotlin/io/element/android/x/MainNode.kt index 2fd36db3b9..52668ff3a4 100644 --- a/app/src/main/kotlin/io/element/android/x/MainNode.kt +++ b/app/src/main/kotlin/io/element/android/x/MainNode.kt @@ -21,8 +21,8 @@ import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.core.plugin.Plugin import io.element.android.appnav.RootFlowNode import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.di.DaggerComponentOwner +import io.element.android.libraries.di.DependencyInjectionGraphOwner +import io.element.android.libraries.di.annotations.ApplicationContext import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @@ -38,8 +38,8 @@ class MainNode( buildContext = buildContext, plugins = plugins, ), - DaggerComponentOwner { - override val daggerComponent = (context as DaggerComponentOwner).daggerComponent + DependencyInjectionGraphOwner { + override val graph = (context as DependencyInjectionGraphOwner).graph override fun resolve(navTarget: RootNavTarget, buildContext: BuildContext): Node { return createNode(buildContext = buildContext) 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 8525f6356b..5c74510e49 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 @@ -7,7 +7,8 @@ package io.element.android.x.di -import com.squareup.anvil.annotations.ContributesTo +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesTo import io.element.android.features.api.MigrationEntryPoint import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.lockscreen.api.LockScreenEntryPoint @@ -15,7 +16,6 @@ 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 diff --git a/app/src/main/kotlin/io/element/android/x/di/AppComponent.kt b/app/src/main/kotlin/io/element/android/x/di/AppComponent.kt deleted file mode 100644 index 2a0cac9d74..0000000000 --- a/app/src/main/kotlin/io/element/android/x/di/AppComponent.kt +++ /dev/null @@ -1,28 +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.x.di - -import android.content.Context -import com.squareup.anvil.annotations.MergeComponent -import dagger.BindsInstance -import io.element.android.libraries.architecture.NodeFactoriesBindings -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.di.SingleIn - -@SingleIn(AppScope::class) -@MergeComponent(AppScope::class) -interface AppComponent : NodeFactoriesBindings { - @MergeComponent.Factory - interface Factory { - fun create( - @ApplicationContext @BindsInstance - context: Context - ): AppComponent - } -} diff --git a/app/src/main/kotlin/io/element/android/x/di/AppGraph.kt b/app/src/main/kotlin/io/element/android/x/di/AppGraph.kt new file mode 100644 index 0000000000..195265a99c --- /dev/null +++ b/app/src/main/kotlin/io/element/android/x/di/AppGraph.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.di + +import android.content.Context +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.DependencyGraph +import dev.zacsweers.metro.Provides +import io.element.android.libraries.architecture.NodeFactoriesBindings +import io.element.android.libraries.di.annotations.ApplicationContext + +@DependencyGraph(AppScope::class) +interface AppGraph : NodeFactoriesBindings { + val sessionGraphFactory: SessionGraph.Factory + + @DependencyGraph.Factory + interface Factory { + fun create( + @ApplicationContext @Provides + context: Context + ): AppGraph + } +} 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 c5bc3f4598..d98a05321d 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 @@ -11,9 +11,11 @@ import android.content.Context import android.content.SharedPreferences import android.content.res.Resources import androidx.preference.PreferenceManager -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn import io.element.android.appconfig.ApplicationConfig import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.messages.impl.timeline.components.customreaction.DefaultEmojibaseProvider @@ -23,24 +25,23 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.BaseDirectory 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.libraries.di.annotations.ApplicationContext import io.element.android.x.BuildConfig import io.element.android.x.R import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.plus import java.io.File -@Module +@BindingContainer @ContributesTo(AppScope::class) object AppModule { @Provides + @BaseDirectory fun providesBaseDirectory(@ApplicationContext context: Context): File { return File(context.filesDir, "sessions") } @@ -105,11 +106,7 @@ object AppModule { @Provides @SingleIn(AppScope::class) fun providesCoroutineDispatchers(): CoroutineDispatchers { - return CoroutineDispatchers( - io = Dispatchers.IO, - computation = Dispatchers.Default, - main = Dispatchers.Main, - ) + return CoroutineDispatchers.Default } @Provides diff --git a/app/src/main/kotlin/io/element/android/x/di/DefaultRoomComponentFactory.kt b/app/src/main/kotlin/io/element/android/x/di/DefaultRoomGraphFactory.kt similarity index 52% rename from app/src/main/kotlin/io/element/android/x/di/DefaultRoomComponentFactory.kt rename to app/src/main/kotlin/io/element/android/x/di/DefaultRoomGraphFactory.kt index fb7a24ae8a..9ae8c54eb7 100644 --- a/app/src/main/kotlin/io/element/android/x/di/DefaultRoomComponentFactory.kt +++ b/app/src/main/kotlin/io/element/android/x/di/DefaultRoomGraphFactory.kt @@ -7,20 +7,19 @@ package io.element.android.x.di -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.appnav.di.RoomComponentFactory +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.appnav.di.RoomGraphFactory import io.element.android.libraries.di.SessionScope 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 { +@Inject +class DefaultRoomGraphFactory( + private val sessionGraph: SessionGraph, +) : RoomGraphFactory { override fun create(room: JoinedRoom): Any { - return roomComponentBuilder - .joinedRoom(room) - .baseRoom(room) - .build() + return sessionGraph.roomGraphFactory + .create(room, room) } } diff --git a/app/src/main/kotlin/io/element/android/x/di/DefaultSessionComponentFactory.kt b/app/src/main/kotlin/io/element/android/x/di/DefaultSessionComponentFactory.kt deleted file mode 100644 index 1c36991cd2..0000000000 --- a/app/src/main/kotlin/io/element/android/x/di/DefaultSessionComponentFactory.kt +++ /dev/null @@ -1,23 +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.x.di - -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.appnav.di.SessionComponentFactory -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.matrix.api.MatrixClient -import javax.inject.Inject - -@ContributesBinding(AppScope::class) -class DefaultSessionComponentFactory @Inject constructor( - private val sessionComponentBuilder: SessionComponent.Builder -) : SessionComponentFactory { - override fun create(client: MatrixClient): Any { - return sessionComponentBuilder.client(client).build() - } -} diff --git a/app/src/main/kotlin/io/element/android/x/di/DefaultSessionGraphFactory.kt b/app/src/main/kotlin/io/element/android/x/di/DefaultSessionGraphFactory.kt new file mode 100644 index 0000000000..632e4dd32d --- /dev/null +++ b/app/src/main/kotlin/io/element/android/x/di/DefaultSessionGraphFactory.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.x.di + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.appnav.di.SessionGraphFactory +import io.element.android.libraries.matrix.api.MatrixClient + +@ContributesBinding(AppScope::class) +@Inject +class DefaultSessionGraphFactory( + private val appGraph: AppGraph +) : SessionGraphFactory { + override fun create(client: MatrixClient): Any { + return appGraph.sessionGraphFactory.create(client) + } +} 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 deleted file mode 100644 index ac126ce0f0..0000000000 --- a/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt +++ /dev/null @@ -1,38 +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.x.di - -import com.squareup.anvil.annotations.ContributesTo -import com.squareup.anvil.annotations.MergeSubcomponent -import dagger.BindsInstance -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.BaseRoom -import io.element.android.libraries.matrix.api.room.JoinedRoom - -@SingleIn(RoomScope::class) -@MergeSubcomponent(RoomScope::class) -interface RoomComponent : NodeFactoriesBindings { - @MergeSubcomponent.Builder - interface Builder { - @BindsInstance - fun joinedRoom(room: JoinedRoom): Builder - - @BindsInstance - fun baseRoom(room: BaseRoom): Builder - - fun build(): RoomComponent - } - - @ContributesTo(SessionScope::class) - interface ParentBindings { - fun roomComponentBuilder(): Builder - } -} diff --git a/app/src/main/kotlin/io/element/android/x/di/RoomGraph.kt b/app/src/main/kotlin/io/element/android/x/di/RoomGraph.kt new file mode 100644 index 0000000000..e48dd52daf --- /dev/null +++ b/app/src/main/kotlin/io/element/android/x/di/RoomGraph.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.x.di + +import dev.zacsweers.metro.GraphExtension +import dev.zacsweers.metro.Provides +import io.element.android.libraries.architecture.NodeFactoriesBindings +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom + +@GraphExtension(RoomScope::class) +interface RoomGraph : NodeFactoriesBindings { + @GraphExtension.Factory + interface Factory { + fun create( + @Provides joinedRoom: JoinedRoom, + @Provides baseRoom: BaseRoom + ): RoomGraph + } +} diff --git a/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt b/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt deleted file mode 100644 index 7cdc686917..0000000000 --- a/app/src/main/kotlin/io/element/android/x/di/SessionComponent.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.x.di - -import com.squareup.anvil.annotations.ContributesTo -import com.squareup.anvil.annotations.MergeSubcomponent -import dagger.BindsInstance -import io.element.android.libraries.architecture.NodeFactoriesBindings -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.matrix.api.MatrixClient - -@SingleIn(SessionScope::class) -@MergeSubcomponent(SessionScope::class) -interface SessionComponent : NodeFactoriesBindings { - @MergeSubcomponent.Builder - interface Builder { - @BindsInstance - fun client(matrixClient: MatrixClient): Builder - - fun build(): SessionComponent - } - - @ContributesTo(AppScope::class) - interface ParentBindings { - fun sessionComponentBuilder(): Builder - } -} diff --git a/app/src/main/kotlin/io/element/android/x/di/SessionGraph.kt b/app/src/main/kotlin/io/element/android/x/di/SessionGraph.kt new file mode 100644 index 0000000000..3782b00a58 --- /dev/null +++ b/app/src/main/kotlin/io/element/android/x/di/SessionGraph.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.x.di + +import dev.zacsweers.metro.GraphExtension +import dev.zacsweers.metro.Provides +import io.element.android.libraries.architecture.NodeFactoriesBindings +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient + +@GraphExtension(SessionScope::class) +interface SessionGraph : NodeFactoriesBindings { + val roomGraphFactory: RoomGraph.Factory + + @GraphExtension.Factory + interface Factory { + fun create(@Provides matrixClient: MatrixClient): SessionGraph + } +} diff --git a/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt index 72e864bbea..d2fbb1b78f 100644 --- a/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt +++ b/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt @@ -10,10 +10,14 @@ package io.element.android.x.initializer import android.content.Context import androidx.startup.Initializer import io.element.android.features.rageshake.impl.crash.VectorUncaughtExceptionHandler +import io.element.android.features.rageshake.impl.di.RageshakeBindings +import io.element.android.libraries.architecture.bindings class CrashInitializer : Initializer { override fun create(context: Context) { - VectorUncaughtExceptionHandler(context).activate() + VectorUncaughtExceptionHandler( + context.bindings().preferencesCrashDataStore(), + ).activate() } override fun dependencies(): List>> = emptyList() diff --git a/app/src/main/kotlin/io/element/android/x/intent/DefaultIntentProvider.kt b/app/src/main/kotlin/io/element/android/x/intent/DefaultIntentProvider.kt index fedcdf2919..746f570447 100644 --- a/app/src/main/kotlin/io/element/android/x/intent/DefaultIntentProvider.kt +++ b/app/src/main/kotlin/io/element/android/x/intent/DefaultIntentProvider.kt @@ -10,19 +10,20 @@ package io.element.android.x.intent import android.content.Context import android.content.Intent import androidx.core.net.toUri -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.deeplink.DeepLinkCreator -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.deeplink.api.DeepLinkCreator +import io.element.android.libraries.di.annotations.ApplicationContext 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.push.impl.intent.IntentProvider import io.element.android.x.MainActivity -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultIntentProvider @Inject constructor( +@Inject +class DefaultIntentProvider( @ApplicationContext private val context: Context, private val deepLinkCreator: DeepLinkCreator, ) : IntentProvider { @@ -33,7 +34,7 @@ class DefaultIntentProvider @Inject constructor( ): Intent { return Intent(context, MainActivity::class.java).apply { action = Intent.ACTION_VIEW - data = deepLinkCreator.room(sessionId, roomId, threadId).toUri() + data = deepLinkCreator.create(sessionId, roomId, threadId).toUri() } } } 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 index 4a103b4daa..20ac5c2476 100644 --- a/app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.kt +++ b/app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.kt @@ -7,15 +7,16 @@ package io.element.android.x.oidc -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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( +@Inject +class DefaultOidcRedirectUrlProvider( private val stringProvider: StringProvider, ) : OidcRedirectUrlProvider { override fun provide() = buildString { diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index 73a2ed405c..72a7d32cc0 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -9,6 +9,7 @@ + @@ -19,6 +20,7 @@ + diff --git a/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt b/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt index 5e81d6b964..9d6d9d4320 100644 --- a/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt +++ b/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt @@ -5,15 +5,22 @@ * Please see LICENSE files in the repository root for full details. */ +@file:Suppress("SameParameterValue") + package io.element.android.x.intent import android.content.Context import android.content.Intent import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.deeplink.DeepLinkCreator +import io.element.android.libraries.deeplink.api.DeepLinkCreator +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.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_THREAD_ID +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.x.MainActivity import org.junit.Test import org.junit.runner.RunWith @@ -23,45 +30,31 @@ import org.robolectric.RuntimeEnvironment @RunWith(RobolectricTestRunner::class) class DefaultIntentProviderTest { @Test - fun `test getViewRoomIntent with Session`() { - val sut = createDefaultIntentProvider() - val result = sut.getViewRoomIntent( - sessionId = A_SESSION_ID, - roomId = null, - threadId = null, + fun `test getViewRoomIntent with data`() { + val deepLinkCreator = lambdaRecorder { _, _, _ -> "deepLinkCreatorResult" } + val sut = createDefaultIntentProvider( + deepLinkCreator = { sessionId, roomId, threadId -> deepLinkCreator.invoke(sessionId, roomId, threadId) }, ) - result.commonAssertions() - assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org") - } - - @Test - fun `test getViewRoomIntent with Session and Room`() { - val sut = createDefaultIntentProvider() - val result = sut.getViewRoomIntent( - sessionId = A_SESSION_ID, - roomId = A_ROOM_ID, - threadId = null, - ) - result.commonAssertions() - assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain") - } - - @Test - fun `test getViewRoomIntent with Session, Room and Thread`() { - val sut = createDefaultIntentProvider() val result = sut.getViewRoomIntent( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID, ) result.commonAssertions() - assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId") + assertThat(result.data.toString()).isEqualTo("deepLinkCreatorResult") + deepLinkCreator.assertions().isCalledOnce().with( + value(A_SESSION_ID), + value(A_ROOM_ID), + value(A_THREAD_ID), + ) } - private fun createDefaultIntentProvider(): DefaultIntentProvider { + private fun createDefaultIntentProvider( + deepLinkCreator: DeepLinkCreator = DeepLinkCreator { _, _, _ -> "" }, + ): DefaultIntentProvider { return DefaultIntentProvider( context = RuntimeEnvironment.getApplication() as Context, - deepLinkCreator = DeepLinkCreator(), + deepLinkCreator = deepLinkCreator, ) } diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index bc0fa405a7..7f487a1e63 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -8,7 +8,8 @@ @file:Suppress("UnstableApiUsage") import extension.allFeaturesApi -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies plugins { id("io.element.android-compose-library") @@ -19,15 +20,17 @@ android { namespace = "io.element.android.appnav" } -setupAnvil() +setupDependencyInjection() dependencies { allFeaturesApi(project) implementation(projects.libraries.core) + implementation(projects.libraries.accountselect.api) implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) - implementation(projects.libraries.deeplink) + implementation(projects.libraries.deeplink.api) + implementation(projects.libraries.featureflag.api) implementation(projects.libraries.matrix.api) implementation(projects.libraries.oidc.api) implementation(projects.libraries.preferences.api) @@ -35,6 +38,7 @@ dependencies { implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.matrixui) + implementation(projects.libraries.uiCommon) implementation(projects.libraries.uiStrings) implementation(projects.features.login.api) @@ -42,18 +46,12 @@ dependencies { implementation(projects.features.ftue.api) implementation(projects.features.share.api) - implementation(projects.features.viewfolder.api) implementation(projects.services.apperror.impl) implementation(projects.services.appnavstate.api) implementation(projects.services.analytics.api) - testImplementation(libs.test.junit) - testImplementation(libs.test.robolectric) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs) testImplementation(projects.features.login.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.oidc.test) @@ -61,11 +59,8 @@ dependencies { testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.features.networkmonitor.test) - testImplementation(projects.tests.testutils) testImplementation(projects.features.rageshake.test) testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.analytics.test) testImplementation(projects.services.toolbox.test) - testImplementation(libs.test.appyx.junit) - testImplementation(libs.test.arch.core) } 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 ae169fcd34..290a351e86 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt @@ -22,29 +22,30 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode 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.appnav.di.SessionComponentFactory +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.appnav.di.SessionGraphFactory import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.DaggerComponentOwner +import io.element.android.libraries.di.DependencyInjectionGraphOwner import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder import kotlinx.parcelize.Parcelize /** - * `LoggedInAppScopeFlowNode` is a Node responsible to set up the Dagger + * `LoggedInAppScopeFlowNode` is a Node responsible to set up the Session graph. * [io.element.android.libraries.di.SessionScope]. It has only one child: [LoggedInFlowNode]. * This allow to inject objects with SessionScope in the constructor of [LoggedInFlowNode]. */ @ContributesNode(AppScope::class) -class LoggedInAppScopeFlowNode @AssistedInject constructor( +@AssistedInject +class LoggedInAppScopeFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - sessionComponentFactory: SessionComponentFactory, + sessionGraphFactory: SessionGraphFactory, private val imageLoaderHolder: ImageLoaderHolder, ) : ParentNode( navModel = PermanentNavModel( @@ -53,9 +54,10 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor( ), buildContext = buildContext, plugins = plugins -), DaggerComponentOwner { +), DependencyInjectionGraphOwner { interface Callback : Plugin { fun onOpenBugReport() + fun onAddAccount() } @Parcelize @@ -66,7 +68,7 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor( ) : NodeInputs private val inputs: Inputs = inputs() - override val daggerComponent = sessionComponentFactory.create(inputs.matrixClient) + override val graph = sessionGraphFactory.create(inputs.matrixClient) override fun onBuilt() { super.onBuilt() @@ -82,6 +84,10 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor( override fun onOpenBugReport() { plugins().forEach { it.onOpenBugReport() } } + + override fun onAddAccount() { + plugins().forEach { it.onAddAccount() } + } } return createNode(buildContext, listOf(callback)) } 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 3f403ea8e0..c557d6e1c2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt @@ -7,6 +7,7 @@ package io.element.android.appnav +import dev.zacsweers.metro.Inject 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 @@ -18,9 +19,9 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import javax.inject.Inject -class LoggedInEventProcessor @Inject constructor( +@Inject +class LoggedInEventProcessor( private val snackbarDispatcher: SnackbarDispatcher, private val roomMembershipObserver: RoomMembershipObserver, ) { @@ -30,9 +31,17 @@ class LoggedInEventProcessor @Inject constructor( observingJob = roomMembershipObserver.updates .filter { !it.isUserInRoom } .distinctUntilChanged() - .onEach { - when (it.change) { - MembershipChange.LEFT -> displayMessage(CommonStrings.common_current_user_left_room) + .onEach { roomMemberShipUpdate -> + when (roomMemberShipUpdate.change) { + MembershipChange.LEFT -> { + displayMessage( + if (roomMemberShipUpdate.isSpace) { + CommonStrings.common_current_user_left_space + } else { + 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 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 f621c7a090..66df9a6dd5 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -36,10 +36,10 @@ import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.replace import com.bumble.appyx.navmodel.backstack.operation.singleTop -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.JoinedRoom -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.appnav.loggedin.LoggedInNode import io.element.android.appnav.loggedin.MediaPreviewConfigMigration import io.element.android.appnav.loggedin.SendQueues @@ -51,7 +51,6 @@ 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 @@ -59,6 +58,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.features.share.api.ShareEntryPoint +import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.features.startchat.api.StartChatEntryPoint import io.element.android.features.userprofile.api.UserProfileEntryPoint import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint @@ -67,7 +67,6 @@ import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode 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 @@ -75,13 +74,13 @@ 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.verification.SessionVerificationServiceListener import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService +import io.element.android.libraries.ui.common.nodes.emptyNode import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first @@ -100,7 +99,8 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.toKotlinDuration @ContributesNode(SessionScope::class) -class LoggedInFlowNode @AssistedInject constructor( +@AssistedInject +class LoggedInFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val homeEntryPoint: HomeEntryPoint, @@ -117,7 +117,6 @@ class LoggedInFlowNode @AssistedInject constructor( private val shareEntryPoint: ShareEntryPoint, private val matrixClient: MatrixClient, private val sendingQueue: SendQueues, - private val logoutEntryPoint: LogoutEntryPoint, private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint, private val mediaPreviewConfigMigration: MediaPreviewConfigMigration, private val sessionEnterpriseService: SessionEnterpriseService, @@ -138,6 +137,7 @@ class LoggedInFlowNode @AssistedInject constructor( ) { interface Callback : Plugin { fun onOpenBugReport() + fun onAddAccount() } private val loggedInFlowProcessor = LoggedInEventProcessor( @@ -275,16 +275,13 @@ class LoggedInFlowNode @AssistedInject constructor( @Parcelize data class IncomingShare(val intent: Intent) : NavTarget - @Parcelize - data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget - @Parcelize data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.Placeholder -> createNode(buildContext) + NavTarget.Placeholder -> emptyNode(buildContext) NavTarget.LoggedInPermanent -> { val callback = object : LoggedInNode.Callback { override fun navigateToNotificationTroubleshoot() { @@ -322,10 +319,6 @@ class LoggedInFlowNode @AssistedInject constructor( override fun onReportBugClick() { plugins().forEach { it.onOpenBugReport() } } - - override fun onLogoutForNativeSlidingSyncMigrationNeeded() { - backstack.push(NavTarget.LogoutForNativeSlidingSyncMigrationNeeded) - } } homeEntryPoint .nodeBuilder(this, buildContext) @@ -333,7 +326,7 @@ class LoggedInFlowNode @AssistedInject constructor( .build() } is NavTarget.Room -> { - val callback = object : JoinedRoomLoadedFlowNode.Callback { + val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback { override fun onOpenRoom(roomId: RoomId, serverNames: List) { backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames)) } @@ -372,6 +365,11 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings)) } } + val spaceCallback = object : SpaceEntryPoint.Callback { + override fun onOpenRoom(roomId: RoomId, viaParameters: List) { + backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames = viaParameters)) + } + } val inputs = RoomFlowNode.Inputs( roomIdOrAlias = navTarget.roomIdOrAlias, roomDescription = Optional.ofNullable(navTarget.roomDescription), @@ -379,7 +377,7 @@ class LoggedInFlowNode @AssistedInject constructor( trigger = Optional.ofNullable(navTarget.trigger), initialElement = navTarget.initialElement ) - createNode(buildContext, plugins = listOf(inputs, callback)) + createNode(buildContext, plugins = listOf(inputs, joinedRoomCallback, spaceCallback)) } is NavTarget.UserProfile -> { val callback = object : UserProfileEntryPoint.Callback { @@ -394,6 +392,10 @@ class LoggedInFlowNode @AssistedInject constructor( } is NavTarget.Settings -> { val callback = object : PreferencesEntryPoint.Callback { + override fun onAddAccount() { + plugins().forEach { it.onAddAccount() } + } + override fun onOpenBugReport() { plugins().forEach { it.onOpenBugReport() } } @@ -406,11 +408,7 @@ class LoggedInFlowNode @AssistedInject constructor( 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") - } + override fun navigateTo(roomId: RoomId, eventId: EventId) { backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Messages(eventId))) } } @@ -447,8 +445,7 @@ class LoggedInFlowNode @AssistedInject constructor( .build() } NavTarget.Ftue -> { - ftueEntryPoint.nodeBuilder(this, buildContext) - .build() + ftueEntryPoint.createNode(this, buildContext) } NavTarget.RoomDirectorySearch -> { roomDirectoryEntryPoint.nodeBuilder(this, buildContext) @@ -479,17 +476,6 @@ class LoggedInFlowNode @AssistedInject constructor( .params(ShareEntryPoint.Params(intent = navTarget.intent)) .build() } - is NavTarget.LogoutForNativeSlidingSyncMigrationNeeded -> { - val callback = object : LogoutEntryPoint.Callback { - override fun onChangeRecoveryKeyClick() { - backstack.push(NavTarget.SecureBackup()) - } - } - - logoutEntryPoint.nodeBuilder(this, buildContext) - .callback(callback) - .build() - } is NavTarget.IncomingVerificationRequest -> { incomingVerificationEntryPoint.nodeBuilder(this, buildContext) .params(IncomingVerificationEntryPoint.Params(navTarget.data)) @@ -560,12 +546,6 @@ class LoggedInFlowNode @AssistedInject constructor( } } } - - @ContributesNode(AppScope::class) - class PlaceholderNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - ) : Node(buildContext, plugins = plugins) } @Parcelize 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 bd4802b77b..5da44f715d 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt @@ -20,9 +20,10 @@ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.api.LoginParams import io.element.android.libraries.architecture.BackstackView @@ -31,12 +32,12 @@ 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 import io.element.android.libraries.matrix.ui.media.NotLoggedInImageLoaderFactory import kotlinx.parcelize.Parcelize @ContributesNode(AppScope::class) -class NotLoggedInFlowNode @AssistedInject constructor( +@AssistedInject +class NotLoggedInFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val loginEntryPoint: LoginEntryPoint, 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 3d02739ba6..93df005c76 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -9,24 +9,25 @@ package io.element.android.appnav import android.content.Intent import android.os.Parcelable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.node.node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.state.MutableSavedStateMap import com.bumble.appyx.navmodel.backstack.BackStack 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 com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader +import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackSlider +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.JoinedRoom -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.appnav.di.MatrixSessionCache import io.element.android.appnav.intent.IntentResolver import io.element.android.appnav.intent.ResolvedIntent @@ -38,43 +39,46 @@ import io.element.android.features.login.api.accesscontrol.AccountProviderAccess import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.signedout.api.SignedOutEntryPoint -import io.element.android.features.viewfolder.api.ViewFolderEntryPoint +import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.appyx.rememberDelegateTransitionHandler 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 -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.deeplink.api.DeeplinkData +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oidc.api.OidcActionFlow import io.element.android.libraries.sessionstorage.api.LoggedInState +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.ui.common.nodes.emptyNode import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import timber.log.Timber -@ContributesNode(AppScope::class) -class RootFlowNode @AssistedInject constructor( +@ContributesNode(AppScope::class) @AssistedInject class RootFlowNode( @Assisted val buildContext: BuildContext, @Assisted plugins: List, - private val authenticationService: MatrixAuthenticationService, + private val sessionStore: SessionStore, private val accountProviderAccessControl: AccountProviderAccessControl, private val navStateFlowFactory: RootNavStateFlowFactory, private val matrixSessionCache: MatrixSessionCache, private val presenter: RootPresenter, private val bugReportEntryPoint: BugReportEntryPoint, - private val viewFolderEntryPoint: ViewFolderEntryPoint, private val signedOutEntryPoint: SignedOutEntryPoint, + private val accountSelectEntryPoint: AccountSelectEntryPoint, private val intentResolver: IntentResolver, private val oidcActionFlow: OidcActionFlow, private val bugReporter: BugReporter, + private val featureFlagService: FeatureFlagService, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.SplashScreen, @@ -96,27 +100,24 @@ class RootFlowNode @AssistedInject constructor( } private fun observeNavState() { - navStateFlowFactory.create(buildContext.savedStateMap) - .distinctUntilChanged() - .onEach { navState -> - Timber.v("navState=$navState") - when (navState.loggedInState) { - is LoggedInState.LoggedIn -> { - if (navState.loggedInState.isTokenValid) { - tryToRestoreLatestSession( - onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) }, - onFailure = { switchToNotLoggedInFlow(null) } - ) - } else { - switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId)) - } - } - LoggedInState.NotLoggedIn -> { - switchToNotLoggedInFlow(null) + navStateFlowFactory.create(buildContext.savedStateMap).distinctUntilChanged().onEach { navState -> + Timber.v("navState=$navState") + when (navState.loggedInState) { + is LoggedInState.LoggedIn -> { + if (navState.loggedInState.isTokenValid) { + tryToRestoreLatestSession( + onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) }, + onFailure = { switchToNotLoggedInFlow(null) } + ) + } else { + switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId)) } } + LoggedInState.NotLoggedIn -> { + switchToNotLoggedInFlow(null) + } } - .launchIn(lifecycleScope) + }.launchIn(lifecycleScope) } private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) { @@ -138,22 +139,19 @@ class RootFlowNode @AssistedInject constructor( onFailure: () -> Unit, onSuccess: (SessionId) -> Unit, ) { - matrixSessionCache.getOrRestore(sessionId) - .onSuccess { - Timber.v("Succeed to restore session $sessionId") - onSuccess(sessionId) - } - .onFailure { - Timber.e(it, "Failed to restore session $sessionId") - onFailure() - } + matrixSessionCache.getOrRestore(sessionId).onSuccess { + Timber.v("Succeed to restore session $sessionId") + onSuccess(sessionId) + }.onFailure { + Timber.e(it, "Failed to restore session $sessionId") + onFailure() + } } private suspend fun tryToRestoreLatestSession( - onSuccess: (SessionId) -> Unit, - onFailure: () -> Unit + onSuccess: (SessionId) -> Unit, onFailure: () -> Unit ) { - val latestSessionId = authenticationService.getLatestSessionId() + val latestSessionId = sessionStore.getLatestSessionId() if (latestSessionId == null) { onFailure() return @@ -173,50 +171,63 @@ class RootFlowNode @AssistedInject constructor( modifier = modifier, onOpenBugReport = this::onOpenBugReport, ) { - BackstackView() + val backstackSlider = rememberBackstackSlider( + transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) }, + ) + val backstackFader = rememberBackstackFader( + transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) }, + ) + val transitionHandler = rememberDelegateTransitionHandler { navTarget -> + when (navTarget) { + is NavTarget.SplashScreen, + is NavTarget.LoggedInFlow -> backstackFader + else -> backstackSlider + } + } + BackstackView(transitionHandler = transitionHandler) } } sealed interface NavTarget : Parcelable { - @Parcelize - data object SplashScreen : NavTarget + @Parcelize data object SplashScreen : NavTarget - @Parcelize - data class NotLoggedInFlow( + @Parcelize data class AccountSelect( + val currentSessionId: SessionId, + val intent: Intent?, + val permalinkData: PermalinkData?, + ) : NavTarget + + @Parcelize data class NotLoggedInFlow( val params: LoginParams? ) : NavTarget - @Parcelize - data class LoggedInFlow( - val sessionId: SessionId, - val navId: Int + @Parcelize data class LoggedInFlow( + val sessionId: SessionId, val navId: Int ) : NavTarget - @Parcelize - data class SignedOutFlow( + @Parcelize data class SignedOutFlow( val sessionId: SessionId ) : NavTarget - @Parcelize - data object BugReport : NavTarget - - @Parcelize - data class ViewLogs( - val rootPath: String, - ) : NavTarget + @Parcelize data object BugReport : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { is NavTarget.LoggedInFlow -> { - val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also { - Timber.w("Couldn't find any session, go through SplashScreen") - } + val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId) + ?: return emptyNode(buildContext).also { + Timber.w("Couldn't find any session, go through SplashScreen") + } val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient) val callback = object : LoggedInAppScopeFlowNode.Callback { override fun onOpenBugReport() { backstack.push(NavTarget.BugReport) } + + override fun onAddAccount() { + backstack.push(NavTarget.NotLoggedInFlow(null)) + } } createNode(buildContext, plugins = listOf(inputs, callback)) } @@ -232,51 +243,46 @@ class RootFlowNode @AssistedInject constructor( createNode(buildContext, plugins = listOf(params, callback)) } is NavTarget.SignedOutFlow -> { - signedOutEntryPoint.nodeBuilder(this, buildContext) - .params( - SignedOutEntryPoint.Params( - sessionId = navTarget.sessionId - ) + signedOutEntryPoint.nodeBuilder(this, buildContext).params( + SignedOutEntryPoint.Params( + sessionId = navTarget.sessionId ) - .build() + ).build() } - NavTarget.SplashScreen -> splashNode(buildContext) + NavTarget.SplashScreen -> emptyNode(buildContext) NavTarget.BugReport -> { val callback = object : BugReportEntryPoint.Callback { - override fun onBugReportSent() { - backstack.pop() - } - - override fun onViewLogs(basePath: String) { - backstack.push(NavTarget.ViewLogs(rootPath = basePath)) - } - } - bugReportEntryPoint - .nodeBuilder(this, buildContext) - .callback(callback) - .build() - } - is NavTarget.ViewLogs -> { - val callback = object : ViewFolderEntryPoint.Callback { override fun onDone() { backstack.pop() } } - val params = ViewFolderEntryPoint.Params( - rootPath = navTarget.rootPath, - ) - viewFolderEntryPoint - .nodeBuilder(this, buildContext) - .params(params) - .callback(callback) - .build() + bugReportEntryPoint.nodeBuilder(this, buildContext).callback(callback).build() } - } - } + is NavTarget.AccountSelect -> { + val callback: AccountSelectEntryPoint.Callback = object : AccountSelectEntryPoint.Callback { + override fun onSelectAccount(sessionId: SessionId) { + lifecycleScope.launch { + if (sessionId == navTarget.currentSessionId) { + // Ensure that the account selection Node is removed from the backstack + // Do not pop when the account is changed to avoid a UI flicker. + backstack.pop() + } + attachSession(sessionId).apply { + if (navTarget.intent != null) { + attachIncomingShare(navTarget.intent) + } else if (navTarget.permalinkData != null) { + attachPermalinkData(navTarget.permalinkData) + } + } + } + } - private fun splashNode(buildContext: BuildContext) = node(buildContext) { - Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() + override fun onCancel() { + backstack.pop() + } + } + accountSelectEntryPoint.nodeBuilder(this, buildContext).callback(callback).build() + } } } @@ -292,78 +298,129 @@ class RootFlowNode @AssistedInject constructor( } private suspend fun onLoginLink(params: LoginParams) { - // Is there a session already? - val latestSessionId = authenticationService.getLatestSessionId() - if (latestSessionId == null) { - // No session, open login - if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) { - switchToNotLoggedInFlow(params) + if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) { + // Is there a session already? + val sessions = sessionStore.getAllSessions() + if (sessions.isNotEmpty()) { + if (featureFlagService.isFeatureEnabled(FeatureFlags.MultiAccount)) { + val loginHintMatrixId = params.loginHint?.removePrefix("mxid:") + val existingAccount = sessions.find { it.userId == loginHintMatrixId } + if (existingAccount != null) { + // We have an existing account matching the login hint, ensure this is the current session + sessionStore.setLatestSession(existingAccount.userId) + } else { + val latestSessionId = sessions.maxBy { it.lastUsageIndex }.userId + attachSession(SessionId(latestSessionId)) + backstack.push(NavTarget.NotLoggedInFlow(params)) + } + } else { + Timber.w("Login link ignored, multi account is disabled") + } } else { - Timber.w("Login link ignored, we are not allowed to connect to the homeserver") - switchToNotLoggedInFlow(null) + switchToNotLoggedInFlow(params) } } else { - // Just ignore the login link if we already have a session - Timber.w("Login link ignored, we already have a session") + Timber.w("Login link ignored, we are not allowed to connect to the homeserver") } } private suspend fun onIncomingShare(intent: Intent) { // Is there a session already? - val latestSessionId = authenticationService.getLatestSessionId() + val latestSessionId = sessionStore.getLatestSessionId() if (latestSessionId == null) { // No session, open login switchToNotLoggedInFlow(null) } else { - attachSession(latestSessionId) - .attachIncomingShare(intent) + // wait for the current session to be restored + val loggedInFlowNode = attachSession(latestSessionId) + if (sessionStore.getAllSessions().size > 1) { + // Several accounts, let the user choose which one to use + backstack.push( + NavTarget.AccountSelect( + currentSessionId = latestSessionId, + intent = intent, + permalinkData = null, + ) + ) + } else { + // Only one account, directly attach the incoming share node. + loggedInFlowNode.attachIncomingShare(intent) + } } } private suspend fun navigateTo(permalinkData: PermalinkData) { Timber.d("Navigating to $permalinkData") - attachSession(null) - .apply { - when (permalinkData) { - is PermalinkData.FallbackLink -> Unit - is PermalinkData.RoomEmailInviteLink -> Unit - is PermalinkData.RoomLink -> { - attachRoom( - roomIdOrAlias = permalinkData.roomIdOrAlias, - trigger = JoinedRoom.Trigger.MobilePermalink, - serverNames = permalinkData.viaParameters, - eventId = permalinkData.eventId, - clearBackstack = true + // Is there a session already? + val latestSessionId = sessionStore.getLatestSessionId() + if (latestSessionId == null) { + // No session, open login + switchToNotLoggedInFlow(null) + } else { + // wait for the current session to be restored + val loggedInFlowNode = attachSession(latestSessionId) + when (permalinkData) { + is PermalinkData.FallbackLink -> Unit + is PermalinkData.RoomEmailInviteLink -> Unit + else -> { + if (sessionStore.getAllSessions().size > 1) { + // Several accounts, let the user choose which one to use + backstack.push( + NavTarget.AccountSelect( + currentSessionId = latestSessionId, + intent = null, + permalinkData = permalinkData, + ) ) - } - is PermalinkData.UserLink -> { - attachUser(permalinkData.userId) + } else { + // Only one account, directly attach the room or the user node. + loggedInFlowNode.attachPermalinkData(permalinkData) } } } + } + } + + private suspend fun LoggedInFlowNode.attachPermalinkData(permalinkData: PermalinkData) { + when (permalinkData) { + is PermalinkData.FallbackLink -> Unit + is PermalinkData.RoomEmailInviteLink -> Unit + is PermalinkData.RoomLink -> { + attachRoom( + roomIdOrAlias = permalinkData.roomIdOrAlias, + trigger = JoinedRoom.Trigger.MobilePermalink, + serverNames = permalinkData.viaParameters, + eventId = permalinkData.eventId, + clearBackstack = true + ) + } + is PermalinkData.UserLink -> { + attachUser(permalinkData.userId) + } + } } private suspend fun navigateTo(deeplinkData: DeeplinkData) { Timber.d("Navigating to $deeplinkData") - attachSession(deeplinkData.sessionId) - .apply { - when (deeplinkData) { - is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState - is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias(), clearBackstack = true) - } + attachSession(deeplinkData.sessionId).apply { + when (deeplinkData) { + is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState + is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias(), clearBackstack = true) } + } } private fun onOidcAction(oidcAction: OidcAction) { oidcActionFlow.post(oidcAction) } - // [sessionId] will be null for permalink. - private suspend fun attachSession(sessionId: SessionId?): LoggedInFlowNode { - // TODO handle multi-session + private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode { + // Ensure that the session is the latest one + sessionStore.setLatestSession(sessionId.value) return waitForChildAttached { navTarget -> - navTarget is NavTarget.LoggedInFlow && (sessionId == null || navTarget.sessionId == sessionId) - } - .attachSession() + navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId + }.attachSession() } } + +private suspend fun SessionStore.getLatestSessionId() = getLatestSession()?.userId?.let(::SessionId) 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 index 1e173474cc..78b9617eae 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt @@ -10,9 +10,10 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.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 @@ -22,7 +23,6 @@ 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" @@ -33,7 +33,8 @@ private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHold */ @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class MatrixSessionCache @Inject constructor( +@Inject +class MatrixSessionCache( private val authenticationService: MatrixAuthenticationService, private val syncOrchestratorFactory: SyncOrchestrator.Factory, ) : MatrixClientProvider { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/RoomComponentFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/RoomGraphFactory.kt similarity index 91% rename from appnav/src/main/kotlin/io/element/android/appnav/di/RoomComponentFactory.kt rename to appnav/src/main/kotlin/io/element/android/appnav/di/RoomGraphFactory.kt index 4911ab50b5..ae0a3a81f2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/RoomComponentFactory.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/RoomGraphFactory.kt @@ -9,6 +9,6 @@ package io.element.android.appnav.di import io.element.android.libraries.matrix.api.room.JoinedRoom -interface RoomComponentFactory { +fun interface RoomGraphFactory { fun create(room: JoinedRoom): Any } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/SessionComponentFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/SessionGraphFactory.kt similarity index 90% rename from appnav/src/main/kotlin/io/element/android/appnav/di/SessionComponentFactory.kt rename to appnav/src/main/kotlin/io/element/android/appnav/di/SessionGraphFactory.kt index 8800043b28..788a1633df 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/SessionComponentFactory.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/SessionGraphFactory.kt @@ -9,6 +9,6 @@ package io.element.android.appnav.di import io.element.android.libraries.matrix.api.MatrixClient -interface SessionComponentFactory { +interface SessionGraphFactory { fun create(client: MatrixClient): 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 index d3ab19466f..b88c4269a0 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt @@ -8,9 +8,9 @@ package io.element.android.appnav.di import androidx.annotation.VisibleForTesting -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.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 @@ -30,7 +30,8 @@ import java.util.concurrent.atomic.AtomicBoolean import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds -class SyncOrchestrator @AssistedInject constructor( +@AssistedInject +class SyncOrchestrator( @Assisted matrixClient: MatrixClient, private val appForegroundStateService: AppForegroundStateService, private val networkMonitor: NetworkMonitor, 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 786b694c3f..187b8f84b6 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,16 +8,16 @@ package io.element.android.appnav.intent import android.content.Intent +import dev.zacsweers.metro.Inject 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.deeplink.api.DeeplinkData +import io.element.android.libraries.deeplink.api.DeeplinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oidc.api.OidcIntentResolver import timber.log.Timber -import javax.inject.Inject sealed interface ResolvedIntent { data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent @@ -27,7 +27,8 @@ sealed interface ResolvedIntent { data class IncomingShare(val intent: Intent) : ResolvedIntent } -class IntentResolver @Inject constructor( +@Inject +class IntentResolver( private val deeplinkParser: DeeplinkParser, private val loginIntentResolver: LoginIntentResolver, private val oidcIntentResolver: OidcIntentResolver, diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt index b337d32cdf..edc2be05db 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt @@ -13,13 +13,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class LoggedInNode @AssistedInject constructor( +@AssistedInject +class LoggedInNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val loggedInPresenter: LoggedInPresenter, 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 7ef73986ac..1f8be2f673 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 @@ -17,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.CryptoSessionStateChange import im.vector.app.features.analytics.plan.UserProperties import io.element.android.libraries.architecture.AsyncData @@ -42,11 +43,11 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject private val pusherTag = LoggerTag("Pusher", LoggerTag.PushLoggerTag) -class LoggedInPresenter @Inject constructor( +@Inject +class LoggedInPresenter( private val matrixClient: MatrixClient, private val syncService: SyncService, private val pushService: PushService, 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 index d9ed15318a..7916d48171 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigration.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigration.kt @@ -7,6 +7,7 @@ package io.element.android.appnav.loggedin +import dev.zacsweers.metro.Inject 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 @@ -14,13 +15,13 @@ 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( +@Inject +class MediaPreviewConfigMigration( private val mediaPreviewService: MediaPreviewService, private val appPreferencesStore: AppPreferencesStore, @SessionCoroutineScope 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 cbb247569a..bb485fd646 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 @@ -8,9 +8,10 @@ package io.element.android.appnav.loggedin import androidx.annotation.VisibleForTesting +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.networkmonitor.api.NetworkStatus 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.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncState @@ -21,13 +22,13 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber -import javax.inject.Inject @VisibleForTesting const val SEND_QUEUES_RETRY_DELAY_MILLIS = 500L @SingleIn(SessionScope::class) -class SendQueues @Inject constructor( +@Inject +class SendQueues( private val matrixClient: MatrixClient, private val syncService: SyncService, ) { 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 de537f0944..9a84049497 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 @@ -21,16 +21,18 @@ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.JoinedRoom -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.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.features.joinroom.api.JoinRoomEntryPoint import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint +import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint.Params import io.element.android.features.roomdirectory.api.RoomDescription +import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs @@ -52,7 +54,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn @@ -63,7 +64,8 @@ import java.util.Optional import kotlin.jvm.optionals.getOrNull @ContributesNode(SessionScope::class) -class RoomFlowNode @AssistedInject constructor( +@AssistedInject +class RoomFlowNode( @Assisted val buildContext: BuildContext, @Assisted plugins: List, private val client: MatrixClient, @@ -71,6 +73,7 @@ class RoomFlowNode @AssistedInject constructor( private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint, private val syncService: SyncService, private val membershipObserver: RoomMembershipObserver, + private val spaceEntryPoint: SpaceEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Loading, @@ -105,6 +108,9 @@ class RoomFlowNode @AssistedInject constructor( @Parcelize data class JoinedRoom(val roomId: RoomId) : NavTarget + + @Parcelize + data class JoinedSpace(val spaceId: RoomId) : NavTarget } override fun onBuilt() { @@ -142,40 +148,28 @@ class RoomFlowNode @AssistedInject constructor( .withPreviousValue() combine(currentMembershipFlow, isSpaceFlow) { (previousMembership, membership), isSpace -> Timber.d("Room membership: $membership") - when (membership) { - CurrentUserMembership.JOINED -> { - if (isSpace) { - // It should not happen, but probably due to an issue in the sliding sync, - // we can have a space here in case the space has just been joined. - // So navigate to the JoinRoom target for now, which will - // handle the space not supported screen - backstack.newRoot( - NavTarget.JoinRoom( - roomId = roomId, - serverNames = serverNames, - trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite, - ) - ) - } else { - backstack.newRoot(NavTarget.JoinedRoom(roomId)) - } + if (membership == CurrentUserMembership.JOINED) { + if (isSpace) { + backstack.newRoot(NavTarget.JoinedSpace(spaceId = roomId)) + } else { + backstack.newRoot(NavTarget.JoinedRoom(roomId)) } - else -> { - if (membership == CurrentUserMembership.LEFT && previousMembership == CurrentUserMembership.JOINED) { - // The user left the room in this device, remove the room from the backstack - if (!membershipUpdateFlow.first().isUserInRoom) { - navigateUp() - } - } else { - // Was invited or the room is not known, display the join room screen - backstack.newRoot( - NavTarget.JoinRoom( - roomId = roomId, - serverNames = serverNames, - trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite, - ) + } else { + val leavingFromCurrentDevice = + membership == CurrentUserMembership.LEFT && + previousMembership == CurrentUserMembership.JOINED && + membershipUpdateFlow.replayCache.lastOrNull()?.isUserInRoom == false + + if (leavingFromCurrentDevice) { + navigateUp() + } else { + backstack.newRoot( + NavTarget.JoinRoom( + roomId = roomId, + serverNames = serverNames, + trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite, ) - } + ) } } }.launchIn(lifecycleScope) @@ -193,7 +187,7 @@ class RoomFlowNode @AssistedInject constructor( ) } } - val params = RoomAliasResolverEntryPoint.Params(navTarget.roomAlias) + val params = Params(navTarget.roomAlias) roomAliasResolverEntryPoint.nodeBuilder(this, buildContext) .callback(callback) .params(params) @@ -217,6 +211,13 @@ class RoomFlowNode @AssistedInject constructor( ) createNode(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) } + is NavTarget.JoinedSpace -> { + val spaceCallback = plugins().single() + spaceEntryPoint.nodeBuilder(this, buildContext) + .inputs(SpaceEntryPoint.Inputs(roomId = navTarget.spaceId)) + .callback(spaceCallback) + .build() + } } } 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 07580060c7..cbda7a8bfb 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 @@ -24,9 +24,9 @@ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode @@ -45,7 +45,8 @@ import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -class JoinedRoomFlowNode @AssistedInject constructor( +@AssistedInject +class JoinedRoomFlowNode( @Assisted val buildContext: BuildContext, @Assisted plugins: List, loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory, 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 b5d118df3c..f0f8dff3e7 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 @@ -17,10 +17,10 @@ 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.appnav.di.RoomComponentFactory +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.appnav.di.RoomGraphFactory import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint @@ -28,7 +28,7 @@ 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.di.DaggerComponentOwner +import io.element.android.libraries.di.DependencyInjectionGraphOwner import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient @@ -45,7 +45,8 @@ import kotlinx.parcelize.Parcelize import timber.log.Timber @ContributesNode(SessionScope::class) -class JoinedRoomLoadedFlowNode @AssistedInject constructor( +@AssistedInject +class JoinedRoomLoadedFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val messagesEntryPoint: MessagesEntryPoint, @@ -55,7 +56,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( private val sessionCoroutineScope: CoroutineScope, private val matrixClient: MatrixClient, private val activeRoomsHolder: ActiveRoomsHolder, - roomComponentFactory: RoomComponentFactory, + roomGraphFactory: RoomGraphFactory, ) : BaseFlowNode( backstack = BackStack( initialElement = when (val input = plugins.filterIsInstance().first().initialElement) { @@ -67,7 +68,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( ), buildContext = buildContext, plugins = plugins, -), DaggerComponentOwner { +), DependencyInjectionGraphOwner { interface Callback : Plugin { fun onOpenRoom(roomId: RoomId, serverNames: List) fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) @@ -82,7 +83,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( private val inputs: Inputs = inputs() private val callbacks = plugins.filterIsInstance() - override val daggerComponent = roomComponentFactory.create(inputs.room) + override val graph = roomGraphFactory.create(inputs.room) init { lifecycle.subscribe( 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 08e999245b..dfa478d6be 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,18 +9,16 @@ package io.element.android.appnav.root import com.bumble.appyx.core.state.MutableSavedStateMap import com.bumble.appyx.core.state.SavedStateMap +import dev.zacsweers.metro.Inject 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 import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory -import io.element.android.libraries.sessionstorage.api.LoggedInState +import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach -import javax.inject.Inject private const val SAVE_INSTANCE_KEY = "io.element.android.x.RootNavStateFlowFactory.SAVE_INSTANCE_KEY" @@ -28,12 +26,12 @@ private const val SAVE_INSTANCE_KEY = "io.element.android.x.RootNavStateFlowFact * This class is responsible for creating a flow of [RootNavState]. * It gathers data from multiple datasource and creates a unique one. */ -class RootNavStateFlowFactory @Inject constructor( - private val authenticationService: MatrixAuthenticationService, +@Inject +class RootNavStateFlowFactory( + private val sessionStore: SessionStore, private val cacheService: CacheService, private val matrixSessionCache: MatrixSessionCache, private val imageLoaderHolder: ImageLoaderHolder, - private val loginUserStory: LoginUserStory, private val sessionPreferencesStoreFactory: SessionPreferencesStoreFactory, ) { private var currentCacheIndex = 0 @@ -41,14 +39,12 @@ class RootNavStateFlowFactory @Inject constructor( fun create(savedStateMap: SavedStateMap?): Flow { return combine( cacheIndexFlow(savedStateMap), - authenticationService.loggedInStateFlow(), - loginUserStory.loginFlowIsDone, - ) { cacheIndex, loggedInState, loginFlowIsDone -> - if (loginFlowIsDone) { - RootNavState(cacheIndex = cacheIndex, loggedInState = loggedInState) - } else { - RootNavState(cacheIndex = cacheIndex, loggedInState = LoggedInState.NotLoggedIn) - } + sessionStore.loggedInStateFlow(), + ) { cacheIndex, loggedInState -> + RootNavState( + cacheIndex = cacheIndex, + loggedInState = loggedInState, + ) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt index 9d4ce4442d..d987c2a7ec 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.SuperProperties import io.element.android.features.rageshake.api.crash.CrashDetectionState import io.element.android.features.rageshake.api.detection.RageshakeDetectionState @@ -18,9 +19,9 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.SdkMetadata import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.apperror.api.AppErrorStateService -import javax.inject.Inject -class RootPresenter @Inject constructor( +@Inject +class RootPresenter( private val crashDetectionPresenter: Presenter, private val rageshakeDetectionPresenter: Presenter, private val appErrorStateService: AppErrorStateService, diff --git a/appnav/src/main/res/values-de/translations.xml b/appnav/src/main/res/values-de/translations.xml index 339d346a6b..13d085e3dc 100644 --- a/appnav/src/main/res/values-de/translations.xml +++ b/appnav/src/main/res/values-de/translations.xml @@ -1,6 +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." + "%1$s unterstützt das alte Protokoll nicht mehr. Bitte melde dich 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-ko/translations.xml b/appnav/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..a29e74f46c --- /dev/null +++ b/appnav/src/main/res/values-ko/translations.xml @@ -0,0 +1,6 @@ + + + "로그아웃 및 업그레이드" + "%1$s 더 이상 이전 프로토콜을 지원하지 않습니다. 계속 사용하려면 로그아웃 후 다시 로그인해 주세요." + "귀하의 홈서버는 더 이상 이전 프로토콜을 지원하지 않습니다. 앱을 계속 사용하려면 로그아웃한 후 다시 로그인하세요." + diff --git a/appnav/src/main/res/values-pt-rBR/translations.xml b/appnav/src/main/res/values-pt-rBR/translations.xml index 385e07d7d1..90d27d3a44 100644 --- a/appnav/src/main/res/values-pt-rBR/translations.xml +++ b/appnav/src/main/res/values-pt-rBR/translations.xml @@ -1,6 +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." + "%1$s não tem mais suporte ao protocolo antigo. Saia da sua conta e entre novamente para continuar utilizando o aplicativo." + "Seu servidor-casa não é mais compatível com o protocolo antigo. Saia da sua conta e entre 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 d606a8d103..2d84d29475 100644 --- a/appnav/src/main/res/values-pt/translations.xml +++ b/appnav/src/main/res/values-pt/translations.xml @@ -2,5 +2,5 @@ "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." + "O teu servidor já não permite o protocolo antigo. Termine sessão e volte a iniciá-la para continuar a utilizar a aplicação." diff --git a/appnav/src/main/res/values-ro/translations.xml b/appnav/src/main/res/values-ro/translations.xml index bed8d18506..626902b490 100644 --- a/appnav/src/main/res/values-ro/translations.xml +++ b/appnav/src/main/res/values-ro/translations.xml @@ -1,5 +1,6 @@ "Deconectați-vă și faceți upgrade" + "%1$s nu mai acceptă vechiul protocol. Vă rugăm să vă deconectați și să vă reconectați pentru a continua utilizarea aplicației." "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." diff --git a/appnav/src/main/res/values-tr/translations.xml b/appnav/src/main/res/values-tr/translations.xml index c0a8ccd136..443bed5b18 100644 --- a/appnav/src/main/res/values-tr/translations.xml +++ b/appnav/src/main/res/values-tr/translations.xml @@ -1,5 +1,7 @@ "Çıkış Yap ve Yükselt" + "%1$s artık eski protokolü destekleniyor. Uygulamayı kullanmaya devam etmek için lütfen çıkış yapın ve tekrar giriş yapın +" "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/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt index bbaa196520..dfb2638dc1 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt @@ -17,7 +17,7 @@ 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.di.RoomGraphFactory import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.features.messages.api.MessagesEntryPoint @@ -70,7 +70,7 @@ class JoinedRoomLoadedFlowNodeTest { } } - private class FakeRoomComponentFactory : RoomComponentFactory { + private class FakeRoomGraphFactory : RoomGraphFactory { override fun create(room: JoinedRoom): Any { return Unit } @@ -110,7 +110,7 @@ class JoinedRoomLoadedFlowNodeTest { roomDetailsEntryPoint = roomDetailsEntryPoint, appNavigationStateService = FakeAppNavigationStateService(), sessionCoroutineScope = this, - roomComponentFactory = FakeRoomComponentFactory(), + roomGraphFactory = FakeRoomGraphFactory(), matrixClient = FakeMatrixClient(), activeRoomsHolder = activeRoomsHolder, ) 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 e95eb66cc3..def1f33253 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 @@ -14,9 +14,7 @@ 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 +import io.element.android.libraries.deeplink.api.DeeplinkData import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -46,15 +44,11 @@ class IntentResolverTest { @Test fun `test resolve navigation intent root`() { - val sut = createIntentResolver() + val sut = createIntentResolver( + deeplinkParserResult = DeeplinkData.Root(A_SESSION_ID) + ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW - data = DeepLinkCreator().room( - sessionId = A_SESSION_ID, - roomId = null, - threadId = null, - ) - .toUri() } val result = sut.resolve(intent) assertThat(result).isEqualTo( @@ -68,15 +62,15 @@ class IntentResolverTest { @Test fun `test resolve navigation intent room`() { - val sut = createIntentResolver() - val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { - action = Intent.ACTION_VIEW - data = DeepLinkCreator().room( + val sut = createIntentResolver( + deeplinkParserResult = DeeplinkData.Room( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, threadId = null, ) - .toUri() + ) + val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { + action = Intent.ACTION_VIEW } val result = sut.resolve(intent) assertThat(result).isEqualTo( @@ -92,15 +86,15 @@ class IntentResolverTest { @Test fun `test resolve navigation intent thread`() { - val sut = createIntentResolver() - val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { - action = Intent.ACTION_VIEW - data = DeepLinkCreator().room( + val sut = createIntentResolver( + deeplinkParserResult = DeeplinkData.Room( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID, ) - .toUri() + ) + val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { + action = Intent.ACTION_VIEW } val result = sut.resolve(intent) assertThat(result).isEqualTo( @@ -117,7 +111,7 @@ class IntentResolverTest { @Test fun `test resolve oidc`() { val sut = createIntentResolver( - oidcIntentResolverResult = { OidcAction.GoBack }, + oidcIntentResolverResult = { OidcAction.GoBack() }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW @@ -126,7 +120,7 @@ class IntentResolverTest { val result = sut.resolve(intent) assertThat(result).isEqualTo( ResolvedIntent.Oidc( - oidcAction = OidcAction.GoBack + oidcAction = OidcAction.GoBack() ) ) } @@ -240,12 +234,13 @@ class IntentResolverTest { } private fun createIntentResolver( + deeplinkParserResult: DeeplinkData? = null, permalinkParserResult: (String) -> PermalinkData = { lambdaError() }, loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() }, oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() }, ): IntentResolver { return IntentResolver( - deeplinkParser = DeeplinkParser(), + deeplinkParser = { deeplinkParserResult }, loginIntentResolver = FakeLoginIntentResolver( parseResult = loginIntentResolverResult, ), 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 d86c46f159..47cbda4b91 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 @@ -501,22 +501,16 @@ class LoggedInPresenterTest { @Test fun `present - CheckSlidingSyncProxyAvailability forces the sliding sync migration under the right circumstances`() = runTest { - // The migration will be forced if: - // - The user is not using the native sliding sync - // - The sliding sync proxy is no longer supported - // - The native sliding sync is supported + // The migration will be forced if the user is not using the native sliding sync val matrixClient = FakeMatrixClient( currentSlidingSyncVersionLambda = { Result.success(SlidingSyncVersion.Proxy) }, - availableSlidingSyncVersionsLambda = { Result.success(listOf(SlidingSyncVersion.Native)) }, ) createLoggedInPresenter( matrixClient = matrixClient, ).test { val initialState = awaitItem() assertThat(initialState.forceNativeSlidingSyncMigration).isFalse() - initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability) - assertThat(awaitItem().forceNativeSlidingSyncMigration).isTrue() } } diff --git a/build.gradle.kts b/build.gradle.kts index fd8403c66e..7171d5b079 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,24 +5,15 @@ * Please see LICENSE files in the repository root for full details. */ -buildscript { - dependencies { - classpath(libs.kotlin.gradle.plugin) - classpath(libs.gms.google.services) - } -} - // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id("io.element.android-root") + alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.ksp) apply false - alias(libs.plugins.anvil) apply false - alias(libs.plugins.kotlin.jvm) apply false - alias(libs.plugins.kapt) apply false alias(libs.plugins.dependencycheck) apply false alias(libs.plugins.dependencyanalysis) alias(libs.plugins.detekt) @@ -102,6 +93,8 @@ allprojects { // Fix compilation warning for annotations // See https://youtrack.jetbrains.com/issue/KT-73255/Change-defaulting-rule-for-annotations for more details freeCompilerArgs.add("-Xannotation-default-target=first-only") + // Opt-in to context receivers + freeCompilerArgs.add("-Xcontext-parameters") } } } diff --git a/anvilcodegen/.gitignore b/codegen/.gitignore similarity index 100% rename from anvilcodegen/.gitignore rename to codegen/.gitignore diff --git a/anvilcodegen/build.gradle.kts b/codegen/build.gradle.kts similarity index 70% rename from anvilcodegen/build.gradle.kts rename to codegen/build.gradle.kts index 6f73bd9f44..640c9ee366 100644 --- a/anvilcodegen/build.gradle.kts +++ b/codegen/build.gradle.kts @@ -10,11 +10,10 @@ plugins { } dependencies { - implementation(projects.anvilannotations) - api(libs.anvil.compiler.api) - implementation(libs.anvil.compiler.utils) + implementation(projects.annotations) + implementation(libs.metro.runtime) + implementation(libs.kotlin.compiler) implementation(libs.kotlinpoet) - implementation(libs.dagger) implementation(libs.ksp.plugin) implementation(libs.kotlinpoet.ksp) } diff --git a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt b/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt similarity index 78% rename from anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt rename to codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt index 2511e0008d..15bb4afbe0 100644 --- a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt +++ b/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.anvilcodegen +package io.element.android.codegen import com.google.devtools.ksp.KspExperimental import com.google.devtools.ksp.getConstructors @@ -19,7 +19,6 @@ import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.validate -import com.squareup.anvil.annotations.ContributesTo import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec @@ -30,13 +29,14 @@ import com.squareup.kotlinpoet.STAR import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.ksp.toTypeName import com.squareup.kotlinpoet.ksp.writeTo -import dagger.Binds -import dagger.Module -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.multibindings.IntoMap -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.IntoMap +import dev.zacsweers.metro.Origin +import io.element.android.annotations.ContributesNode import org.jetbrains.kotlin.name.FqName class ContributesNodeProcessor( @@ -72,15 +72,16 @@ class ContributesNodeProcessor( val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType val modulePackage = ksClass.packageName.asString() val moduleClassName = "${ksClass.simpleName.asString()}_Module" + val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString()) val content = FileSpec.builder( packageName = modulePackage, fileName = moduleClassName, ) .addType( - TypeSpec.classBuilder(moduleClassName) - .addModifiers(KModifier.ABSTRACT) - .addAnnotation(Module::class) - .addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.toTypeName()).build()) + TypeSpec.interfaceBuilder(moduleClassName) + .addAnnotation(AnnotationSpec.builder(Origin::class).addMember(CLASS_PLACEHOLDER, nodeClassName).build()) + .addAnnotation(BindingContainer::class) + .addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember(CLASS_PLACEHOLDER, scope.toTypeName()).build()) .addFunction( FunSpec.builder("bind${ksClass.simpleName.asString()}Factory") .addModifiers(KModifier.ABSTRACT) @@ -90,7 +91,7 @@ class ContributesNodeProcessor( .addAnnotation(IntoMap::class) .addAnnotation( AnnotationSpec.Companion.builder(ClassName.bestGuess(nodeKeyFqName.asString())).addMember( - "%T::class", + CLASS_PLACEHOLDER, ClassName.bestGuess(ksClass.qualifiedName!!.asString()) ).build() ) @@ -103,7 +104,7 @@ class ContributesNodeProcessor( content.writeTo( codeGenerator = codeGenerator, dependencies = Dependencies( - aggregating = true, + aggregating = false, ksClass.containingFile!! ), ) @@ -113,23 +114,23 @@ class ContributesNodeProcessor( private fun generateFactory(ksClass: KSClassDeclaration) { val generatedPackage = ksClass.packageName.asString() val assistedFactoryClassName = "${ksClass.simpleName.asString()}_AssistedFactory" - val constructor = ksClass.getConstructors().singleOrNull { it.isAnnotationPresent(AssistedInject::class) } - val assistedParameters = constructor?.parameters?.filter { it.isAnnotationPresent(Assisted::class) }.orEmpty() - if (constructor == null || assistedParameters.size != 2) { + val constructor = ksClass.getConstructors().first { it.parameters.isNotEmpty() } + val assistedParameters = constructor.parameters.filter { it.isAnnotationPresent(Assisted::class) } + if (assistedParameters.size != 2) { error( - "${ksClass.qualifiedName} must have an @AssistedInject constructor with 2 @Assisted parameters", + "${ksClass.qualifiedName?.asString()} must have a constructor with 2 @Assisted parameters. Found: ${assistedParameters.size}", ) } val contextAssistedParam = assistedParameters[0] if (contextAssistedParam.name?.asString() != "buildContext") { error( - "${ksClass.qualifiedName} @Assisted parameter must be named buildContext", + "${ksClass.qualifiedName?.asString()} @Assisted parameter must be named buildContext", ) } val pluginsAssistedParam = assistedParameters[1] if (pluginsAssistedParam.name?.asString() != "plugins") { error( - "${ksClass.qualifiedName} @Assisted parameter must be named plugins", + "${ksClass.qualifiedName?.asString()} @Assisted parameter must be named plugins", ) } @@ -140,6 +141,7 @@ class ContributesNodeProcessor( .addType( TypeSpec.interfaceBuilder(assistedFactoryClassName) .addSuperinterface(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(nodeClassName)) + .addAnnotation(AnnotationSpec.builder(Origin::class).addMember("%T::class", nodeClassName).build()) .addAnnotation(AssistedFactory::class) .addFunction( FunSpec.builder("create") @@ -156,13 +158,14 @@ class ContributesNodeProcessor( content.writeTo( codeGenerator = codeGenerator, dependencies = Dependencies( - aggregating = true, + aggregating = false, ksClass.containingFile!! ), ) } companion object { + private const val CLASS_PLACEHOLDER = "%T::class" private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory") private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey") } diff --git a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt b/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessorProvider.kt similarity index 95% rename from anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt rename to codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessorProvider.kt index ec6ad5958e..9631167419 100644 --- a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt +++ b/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessorProvider.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.anvilcodegen +package io.element.android.codegen import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment diff --git a/codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 0000000000..2d18fdfd5c --- /dev/null +++ b/codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +io.element.android.codegen.ContributesNodeProcessorProvider diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md index 84070b5754..6fb5229eeb 100644 --- a/docs/_developer_onboarding.md +++ b/docs/_developer_onboarding.md @@ -249,8 +249,7 @@ Main libraries and frameworks used in this application: - Navigation state with [Appyx](https://bumble-tech.github.io/appyx/). Please watch [this video](https://www.droidcon.com/2022/11/15/model-driven-navigation-with-appyx-from-zero-to-hero/) to learn more about Appyx! -- DI: [Dagger](https://dagger.dev/) and [Anvil](https://github.com/square/anvil). Please - watch [this video](https://www.droidcon.com/2022/06/28/dagger-anvil-learning-to-love-dependency-injection/) to learn more about Anvil! +- Dependency injection: [Metro](https://zacsweers.github.io/metro/latest/) - Reactive State management with Compose runtime and [Molecule](https://github.com/cashapp/molecule) Some patterns are inspired by [Circuit](https://slackhq.github.io/circuit/) @@ -261,7 +260,7 @@ Here are the main points: 2. Views are compose first 3. Presenters are also compose first, and have a single `present(): State` method. It's using the power of compose-runtime/compiler. 4. The point of connection between a `View` and a `Presenter` is a `Node`. -5. A `Node` is also responsible for managing Dagger components if any. +5. A `Node` is also responsible for managing DI graph if any, see for instance `LoggedInAppScopeFlowNode`. 6. A `ParentNode` has some children `Node` and only know about them. 7. This is a single activity full compose application. The `MainActivity` is responsible for holding and configuring the `RootNode`. 8. There is no more needs for Android Architecture Component ViewModel as configuration change should be handled by Composable if needed. @@ -423,7 +422,7 @@ Rageshake can be very useful to get logs from a release version of the applicati - When this is possible, prefer using `sealed interface` instead of `sealed class`; - When writing temporary code, using the string "DO NOT COMMIT" in a comment can help to avoid committing things by mistake. If committed and pushed, the CI will detect this String and will warn the user about it. (TODO Not supported yet!) -- Very occasionally the gradle cache misbehaves and causes problems with Dagger. Try building with `--no-build-cache` if Dagger isn't behaving how you expect. +- Very occasionally the gradle cache misbehaves and causes problems with code generation. Adding `--no-build-cache` to the `gradlew` command line can help to fix compilation issue. ## Happy coding! diff --git a/docs/migration_to_metro.md b/docs/migration_to_metro.md new file mode 100644 index 0000000000..23c5db9709 --- /dev/null +++ b/docs/migration_to_metro.md @@ -0,0 +1,15 @@ +# Migration to Metro + +The dependency injection library is now [Metro](https://zacsweers.github.io/metro/latest/). It replaces both Dagger and Anvil. + +Migration of the current Element X code has been performed in https://github.com/element-hq/element-x-android/pull/5253. + +To migrate other existing code you will need to: + +- replace `setupAnvil()` with `setupDependencyInjection()` in your `build.gradle.kts` files +- replace the Dagger and Anvil imports with Metro ones +- move the `@Inject` apply to the constructor to the class itself (only applicable if there is only one primary constructor +- replace `@AssistedInject` with `@Inject` +- replace `@Module` with `@BindingContainer` + +This should help to migrate your existing code. diff --git a/enterprise b/enterprise index 76e10f6fa4..95789d4011 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit 76e10f6fa4db4196df245a3d29131a95d9e60a4d +Subproject commit 95789d40119499eba8a79284df9dd2306405b099 diff --git a/fastlane/metadata/android/en-US/changelogs/202508040.txt b/fastlane/metadata/android/en-US/changelogs/202508040.txt new file mode 100644 index 0000000000..ac9a4fb45b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202508040.txt @@ -0,0 +1,2 @@ +Main changes in this version: you can now create shortcuts to your recent conversations, several bug fixes related to media processing and downloading. +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/202509000.txt b/fastlane/metadata/android/en-US/changelogs/202509000.txt new file mode 100644 index 0000000000..97562251c5 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202509000.txt @@ -0,0 +1,2 @@ +Main changes in this version: improved timeline loading times, you can now create shortcuts to your recent conversations, several bug fixes related to media processing and downloading. +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/202509010.txt b/fastlane/metadata/android/en-US/changelogs/202509010.txt new file mode 100644 index 0000000000..8955ade680 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202509010.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/202509020.txt b/fastlane/metadata/android/en-US/changelogs/202509020.txt new file mode 100644 index 0000000000..8955ade680 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202509020.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/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsEntryPoint.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsEntryPoint.kt index 404bae44d1..37a78fd727 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsEntryPoint.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsEntryPoint.kt @@ -9,4 +9,4 @@ package io.element.android.features.analytics.api import io.element.android.libraries.architecture.SimpleFeatureEntryPoint -interface AnalyticsEntryPoint : SimpleFeatureEntryPoint +fun interface AnalyticsEntryPoint : SimpleFeatureEntryPoint 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 30fd8cd7e5..bf3584e3b4 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." - "Sie können unsere Bedingungen %1$s lesen." + "Weitere Informationen findest du %1$s." "hier" "Analysedaten teilen" diff --git a/features/analytics/api/src/main/res/values-ko/translations.xml b/features/analytics/api/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..809fc084f7 --- /dev/null +++ b/features/analytics/api/src/main/res/values-ko/translations.xml @@ -0,0 +1,7 @@ + + + "익명화된 사용 데이터를 공유하여 문제점을 파악하는 데 도움을 주십시오." + "모든 이용 약관은 %1$s 에서 확인하실 수 있습니다." + "여기" + "분석 데이터 공유" + 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 ab649cc9ff..01fe907d5f 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 @@ -3,5 +3,5 @@ "Compartilhe dados de uso anônimos para nos ajudar a identificar problemas." "Você pode ler todos os nossos termos %1$s." "aqui" - "Compartilhar dados de utilização" + "Compartilhar dados analíticos" diff --git a/features/analytics/impl/build.gradle.kts b/features/analytics/impl/build.gradle.kts index f78574e51d..ddf093d9c3 100644 --- a/features/analytics/impl/build.gradle.kts +++ b/features/analytics/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -16,7 +17,7 @@ android { namespace = "io.element.android.features.analytics.impl" } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.androidutils) @@ -30,13 +31,7 @@ dependencies { implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.browser) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.test.mockk) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.analytics.test) - testImplementation(projects.tests.testutils) } 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 ba1073d480..b952bdb4e1 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 @@ -14,16 +14,17 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.appconfig.AnalyticsConfig import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab -import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class AnalyticsOptInNode @AssistedInject constructor( +@AssistedInject +class AnalyticsOptInNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: AnalyticsOptInPresenter, 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 39b99a9257..fd590955c8 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 dev.zacsweers.metro.Inject import io.element.android.appconfig.AnalyticsConfig import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.libraries.architecture.Presenter @@ -16,9 +17,9 @@ import io.element.android.libraries.core.meta.BuildMeta import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import javax.inject.Inject -class AnalyticsOptInPresenter @Inject constructor( +@Inject +class AnalyticsOptInPresenter( private val buildMeta: BuildMeta, private val analyticsService: AnalyticsService, ) : Presenter { 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 e6917c237e..ec37542b31 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 @@ -8,9 +8,8 @@ package io.element.android.features.analytics.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import javax.inject.Inject -open class AnalyticsOptInStateProvider @Inject constructor() : PreviewParameterProvider { +open class AnalyticsOptInStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aAnalyticsOptInState(), diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/DefaultAnalyticsEntryPoint.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/DefaultAnalyticsEntryPoint.kt index 4903534159..134535e881 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/DefaultAnalyticsEntryPoint.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/DefaultAnalyticsEntryPoint.kt @@ -9,14 +9,15 @@ package io.element.android.features.analytics.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.analytics.api.AnalyticsEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultAnalyticsEntryPoint @Inject constructor() : AnalyticsEntryPoint { +@Inject +class DefaultAnalyticsEntryPoint : AnalyticsEntryPoint { override fun createNode(parentNode: Node, buildContext: BuildContext): Node { return parentNode.createNode(buildContext) } diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsModule.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsModule.kt index d1ddd57b2f..e5d3342e2d 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsModule.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsModule.kt @@ -7,16 +7,16 @@ package io.element.android.features.analytics.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState import io.element.android.features.analytics.impl.preferences.AnalyticsPreferencesPresenter import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.di.AppScope @ContributesTo(AppScope::class) -@Module +@BindingContainer interface AnalyticsModule { @Binds fun bindAnalyticsPreferencesPresenter(presenter: AnalyticsPreferencesPresenter): Presenter 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 6904734f26..fee6188d23 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 @@ -10,6 +10,7 @@ package io.element.android.features.analytics.impl.preferences import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.rememberCoroutineScope +import dev.zacsweers.metro.Inject import io.element.android.appconfig.AnalyticsConfig import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState @@ -18,9 +19,9 @@ import io.element.android.libraries.core.meta.BuildMeta import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import javax.inject.Inject -class AnalyticsPreferencesPresenter @Inject constructor( +@Inject +class AnalyticsPreferencesPresenter( private val analyticsService: AnalyticsService, private val buildMeta: BuildMeta, ) : Presenter { 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 9f3bd7e652..9fd4dbf0a8 100644 --- a/features/analytics/impl/src/main/res/values-de/translations.xml +++ b/features/analytics/impl/src/main/res/values-de/translations.xml @@ -1,10 +1,10 @@ - "Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile." + "Wir speichern oder profilieren keine personenbezogenen Daten." "Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen." - "Sie können unsere Bedingungen %1$s lesen." + "Weitere Informationen findest du %1$s." "hier" - "Sie können dies jederzeit beenden" + "Du kannst diese Funktion jederzeit deaktivieren" "Wir geben deine Daten nicht an Dritte weiter" "Hilf uns %1$s zu verbessern" diff --git a/features/analytics/impl/src/main/res/values-ko/translations.xml b/features/analytics/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..80dca72120 --- /dev/null +++ b/features/analytics/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,10 @@ + + + "개인 데이터는 기록하거나 프로파일링하지 않습니다." + "익명화된 사용 데이터를 공유하여 문제점을 파악하는 데 도움을 주십시오." + "모든 이용 약관은 %1$s 에서 확인하실 수 있습니다." + "여기" + "이 기능을 언제든지 비활성화할 수 있습니다." + "우리는 귀하의 데이터를 제3자와 공유하지 않습니다." + "%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 b91ab36256..c9a53b8c9f 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 @@ -1,6 +1,6 @@ - "Não registraremos nem criaremos perfil baseado em qualquer dado pessoal" + "Não iremos gravar ou personificar qualquer dado pessoal" "Compartilhe dados de uso anônimos para nos ajudar a identificar problemas." "Você pode ler todos os nossos termos %1$s." "aqui" diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/DefaultAnalyticsEntryPointTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/DefaultAnalyticsEntryPointTest.kt new file mode 100644 index 0000000000..b5ec819632 --- /dev/null +++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/DefaultAnalyticsEntryPointTest.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.analytics.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultAnalyticsEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node creation`() { + val entryPoint = DefaultAnalyticsEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + AnalyticsOptInNode( + buildContext = buildContext, + plugins = plugins, + AnalyticsOptInPresenter( + buildMeta = aBuildMeta(), + analyticsService = FakeAnalyticsService() + ) + ) + } + val result = entryPoint.createNode(parentNode, BuildContext.root(null)) + assertThat(result).isInstanceOf(AnalyticsOptInNode::class.java) + } +} diff --git a/features/cachecleaner/api/build.gradle.kts b/features/cachecleaner/api/build.gradle.kts index 7bfe8f6c5d..81f689bb63 100644 --- a/features/cachecleaner/api/build.gradle.kts +++ b/features/cachecleaner/api/build.gradle.kts @@ -1,4 +1,4 @@ -import extension.setupAnvil +import extension.setupDependencyInjection /* * Copyright 2023, 2024 New Vector Ltd. @@ -15,7 +15,7 @@ android { namespace = "io.element.android.features.cachecleaner.api" } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.architecture) diff --git a/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerBindings.kt b/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerBindings.kt index 9c44c1d5ae..9be2a10525 100644 --- a/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerBindings.kt +++ b/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerBindings.kt @@ -7,8 +7,8 @@ package io.element.android.features.cachecleaner.api -import com.squareup.anvil.annotations.ContributesTo -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesTo @ContributesTo(AppScope::class) interface CacheCleanerBindings { diff --git a/features/cachecleaner/impl/build.gradle.kts b/features/cachecleaner/impl/build.gradle.kts index 137f00e31e..8603de6322 100644 --- a/features/cachecleaner/impl/build.gradle.kts +++ b/features/cachecleaner/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -15,15 +16,12 @@ android { namespace = "io.element.android.features.cachecleaner.impl" } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.cachecleaner.api) implementation(projects.libraries.core) implementation(projects.libraries.architecture) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.truth) - testImplementation(projects.tests.testutils) + testCommonDependencies(libs) } 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 86e6432cc5..fc06174806 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 @@ -7,24 +7,25 @@ package io.element.android.features.cachecleaner.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 import java.io.File -import javax.inject.Inject /** * Default implementation of [CacheCleaner]. */ @ContributesBinding(AppScope::class) -class DefaultCacheCleaner @Inject constructor( +@Inject +class DefaultCacheCleaner( @AppCoroutineScope private val coroutineScope: CoroutineScope, private val dispatchers: CoroutineDispatchers, diff --git a/features/call/api/build.gradle.kts b/features/call/api/build.gradle.kts index 34cb895938..c9c86e8948 100644 --- a/features/call/api/build.gradle.kts +++ b/features/call/api/build.gradle.kts @@ -15,7 +15,6 @@ android { } dependencies { - implementation(projects.anvilannotations) implementation(projects.libraries.architecture) implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) 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 53e1b8c846..8de7a3839b 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 @@ -29,6 +29,7 @@ interface ElementCallEntryPoint { * @param senderName The name of the sender of the event that started the call. * @param avatarUrl The avatar url of the room or DM. * @param timestamp The timestamp of the event that started the call. + * @param expirationTimestamp The timestamp at which the call should stop ringing. * @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. */ @@ -40,6 +41,7 @@ interface ElementCallEntryPoint { senderName: String?, avatarUrl: String?, timestamp: Long, + expirationTimestamp: Long, notificationChannelId: String, textContent: String?, ) diff --git a/features/call/impl/build.gradle.kts b/features/call/impl/build.gradle.kts index 13922366c2..9efe5ba75b 100644 --- a/features/call/impl/build.gradle.kts +++ b/features/call/impl/build.gradle.kts @@ -1,6 +1,7 @@ import extension.buildConfigFieldStr import extension.readLocalProperty -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2024 New Vector Ltd. @@ -60,7 +61,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.appconfig) @@ -87,12 +88,7 @@ dependencies { implementation(libs.element.call.embedded) api(projects.features.call.api) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.mockk) + testCommonDependencies(libs, true) testImplementation(projects.features.call.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.preferences.test) @@ -100,7 +96,5 @@ dependencies { 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) + testImplementation(projects.services.toolbox.test) } 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 009840743c..a1c07462f8 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 @@ -8,20 +8,21 @@ package io.element.android.features.call.impl import android.content.Context -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.call.api.CallType import io.element.android.features.call.api.ElementCallEntryPoint 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.IntentProvider -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultElementCallEntryPoint @Inject constructor( +@Inject +class DefaultElementCallEntryPoint( @ApplicationContext private val context: Context, private val activeCallManager: ActiveCallManager, ) : ElementCallEntryPoint { @@ -42,6 +43,7 @@ class DefaultElementCallEntryPoint @Inject constructor( senderName: String?, avatarUrl: String?, timestamp: Long, + expirationTimestamp: Long, notificationChannelId: String, textContent: String?, ) { @@ -54,6 +56,7 @@ class DefaultElementCallEntryPoint @Inject constructor( senderName = senderName, avatarUrl = avatarUrl, timestamp = timestamp, + expirationTimestamp = expirationTimestamp, notificationChannelId = notificationChannelId, textContent = textContent, ) 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 c18fef410e..5001d598d9 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 @@ -41,5 +41,8 @@ data class WidgetMessage( @SerialName("send_event") SendEvent, + + @SerialName("content_loaded") + ContentLoaded, } } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/di/CallBindings.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/di/CallBindings.kt index e92b4c64fa..887da8d188 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/di/CallBindings.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/di/CallBindings.kt @@ -7,11 +7,11 @@ package io.element.android.features.call.impl.di -import com.squareup.anvil.annotations.ContributesTo +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesTo import io.element.android.features.call.impl.receivers.DeclineCallBroadcastReceiver import io.element.android.features.call.impl.ui.ElementCallActivity import io.element.android.features.call.impl.ui.IncomingCallActivity -import io.element.android.libraries.di.AppScope @ContributesTo(AppScope::class) interface CallBindings { 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 7c97cc8529..0bfbfb0411 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 @@ -26,4 +26,6 @@ data class CallNotificationData( val notificationChannelId: String, val timestamp: Long, val textContent: String?, + // Expiration timestamp in millis since epoch + val expirationTimestamp: Long, ) : 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 de31b0b02b..2f8ce3e53a 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 @@ -15,13 +15,14 @@ import android.provider.Settings import androidx.core.app.NotificationCompat import androidx.core.app.PendingIntentCompat import androidx.core.app.Person +import dev.zacsweers.metro.Inject import io.element.android.appconfig.ElementCallConfig import io.element.android.features.call.api.CallType import io.element.android.features.call.impl.receivers.DeclineCallBroadcastReceiver import io.element.android.features.call.impl.ui.IncomingCallActivity import io.element.android.features.call.impl.utils.IntentProvider import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.ApplicationContext 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 @@ -29,13 +30,13 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader -import javax.inject.Inject import kotlin.time.Duration.Companion.seconds /** * Creates a notification for a ringing call. */ -class RingingCallNotificationCreator @Inject constructor( +@Inject +class RingingCallNotificationCreator( @ApplicationContext private val context: Context, private val matrixClientProvider: MatrixClientProvider, private val imageLoaderHolder: ImageLoaderHolder, @@ -63,6 +64,7 @@ class RingingCallNotificationCreator @Inject constructor( roomAvatarUrl: String?, notificationChannelId: String, timestamp: Long, + expirationTimestamp: Long, textContent: String?, ): Notification? { val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null @@ -87,6 +89,7 @@ class RingingCallNotificationCreator @Inject constructor( notificationChannelId = notificationChannelId, timestamp = timestamp, textContent = textContent, + expirationTimestamp = expirationTimestamp, ) val declineIntent = PendingIntentCompat.getBroadcast( diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt index b64852d283..8cca46f9f4 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt @@ -13,16 +13,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.call.impl.utils.PipController import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.log.logger.LoggerTag import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject private val loggerTag = LoggerTag("PiP") -class PictureInPicturePresenter @Inject constructor( +@Inject +class PictureInPicturePresenter( pipSupportProvider: PipSupportProvider, ) : Presenter { private val isPipSupported = pipSupportProvider.isPipSupported() diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipSupportProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipSupportProvider.kt index 4d5441367e..96e68fabf3 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipSupportProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipSupportProvider.kt @@ -11,11 +11,11 @@ import android.content.Context import android.content.pm.PackageManager import android.os.Build import androidx.annotation.ChecksSdkIntAtLeast -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext -import javax.inject.Inject +import io.element.android.libraries.di.annotations.ApplicationContext interface PipSupportProvider { @ChecksSdkIntAtLeast(Build.VERSION_CODES.O) @@ -23,7 +23,8 @@ interface PipSupportProvider { } @ContributesBinding(AppScope::class) -class DefaultPipSupportProvider @Inject constructor( +@Inject +class DefaultPipSupportProvider( @ApplicationContext private val context: Context, ) : PipSupportProvider { override fun isPipSupported(): Boolean { 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 c857c9e2c8..22b5f0477d 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 @@ -11,6 +11,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.core.content.IntentCompat +import dev.zacsweers.metro.Inject import io.element.android.features.call.api.CallType import io.element.android.features.call.impl.di.CallBindings import io.element.android.features.call.impl.notifications.CallNotificationData @@ -19,7 +20,6 @@ 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 /** * Broadcast receiver to decline the incoming call. 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 0164bb3089..366352a272 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 @@ -17,9 +17,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.compound.theme.ElementTheme import io.element.android.features.call.api.CallType @@ -49,7 +49,8 @@ import timber.log.Timber import java.util.UUID import kotlin.time.Duration.Companion.seconds -class CallScreenPresenter @AssistedInject constructor( +@AssistedInject +class CallScreenPresenter( @Assisted private val callType: CallType, @Assisted private val navigator: CallScreenNavigator, private val callWidgetProvider: CallWidgetProvider, @@ -78,7 +79,7 @@ class CallScreenPresenter @AssistedInject constructor( val urlState = remember { mutableStateOf>(AsyncData.Uninitialized) } val callWidgetDriver = remember { mutableStateOf(null) } val messageInterceptor = remember { mutableStateOf(null) } - var isJoinedCall by rememberSaveable { mutableStateOf(false) } + var isWidgetLoaded by rememberSaveable { mutableStateOf(false) } var ignoreWebViewError by rememberSaveable { mutableStateOf(false) } var webViewError by remember { mutableStateOf(null) } val languageTag = languageTagProvider.provideLanguageTag() @@ -138,8 +139,8 @@ class CallScreenPresenter @AssistedInject constructor( if (parsedMessage?.direction == WidgetMessage.Direction.FromWidget) { if (parsedMessage.action == WidgetMessage.Action.Close) { close(callWidgetDriver.value, navigator) - } else if (parsedMessage.action == WidgetMessage.Action.Join) { - isJoinedCall = true + } else if (parsedMessage.action == WidgetMessage.Action.ContentLoaded) { + isWidgetLoaded = true } } } @@ -150,8 +151,8 @@ class CallScreenPresenter @AssistedInject constructor( // 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.") + if (!isWidgetLoaded) { + Timber.w("The call took too long to load. Displaying an error before exiting.") // This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call webViewError = "" @@ -164,10 +165,10 @@ class CallScreenPresenter @AssistedInject constructor( is CallScreenEvents.Hangup -> { val widgetId = callWidgetDriver.value?.id val interceptor = messageInterceptor.value - if (widgetId != null && interceptor != null && isJoinedCall) { + if (widgetId != null && interceptor != null && isWidgetLoaded) { // If the call was joined, we need to hang up first. Then the UI will be dismissed automatically. sendHangupMessage(widgetId, interceptor) - isJoinedCall = false + isWidgetLoaded = false coroutineScope.launch { // Wait for a couple of seconds to receive the hangup message @@ -197,7 +198,7 @@ class CallScreenPresenter @AssistedInject constructor( urlState = urlState.value, webViewError = webViewError, userAgent = userAgent, - isCallActive = isJoinedCall, + isCallActive = isWidgetLoaded, isInWidgetMode = isInWidgetMode, eventSink = { handleEvents(it) }, ) 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 870c224464..bf210b0a42 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 @@ -30,6 +30,7 @@ import androidx.core.app.PictureInPictureModeChangedInfo import androidx.core.content.IntentCompat import androidx.core.util.Consumer import androidx.lifecycle.Lifecycle +import dev.zacsweers.metro.Inject import io.element.android.features.call.api.CallType import io.element.android.features.call.api.CallType.ExternalUrl import io.element.android.features.call.impl.DefaultElementCallEntryPoint @@ -50,7 +51,6 @@ 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 -import javax.inject.Inject private val loggerTag = LoggerTag("ElementCallActivity") @@ -79,7 +79,7 @@ class ElementCallActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - applicationContext.bindings().inject(this) + bindings().inject(this) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 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 dc77af9b77..daf0e26797 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 @@ -13,6 +13,7 @@ import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.core.content.IntentCompat import androidx.lifecycle.lifecycleScope +import dev.zacsweers.metro.Inject import io.element.android.features.call.api.CallType import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.call.impl.di.CallBindings @@ -30,7 +31,6 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import javax.inject.Inject /** * Activity that's displayed as a full screen intent when an incoming call is received. @@ -64,7 +64,7 @@ class IncomingCallActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - applicationContext.bindings().inject(this) + bindings().inject(this) // Set flags so it can be displayed in the lock screen @Suppress("DEPRECATION") 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 954fa3568f..9483b91a14 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 @@ -176,6 +176,7 @@ internal fun IncomingCallScreenPreview() = ElementPreview { notificationChannelId = "incoming_call", timestamp = 0L, textContent = null, + expirationTimestamp = 1000L, ), onAnswer = {}, onCancel = {}, diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/LanguageTagProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/LanguageTagProvider.kt index cce855139a..74016fd210 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/LanguageTagProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/LanguageTagProvider.kt @@ -9,9 +9,9 @@ package io.element.android.features.call.impl.ui import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalConfiguration -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import javax.inject.Inject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject interface LanguageTagProvider { @Composable @@ -19,7 +19,8 @@ interface LanguageTagProvider { } @ContributesBinding(AppScope::class) -class DefaultLanguageTagProvider @Inject constructor() : LanguageTagProvider { +@Inject +class DefaultLanguageTagProvider : LanguageTagProvider { @Composable override fun provideLanguageTag(): String? { return LocalConfiguration.current.locales.get(0)?.toLanguageTag() 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 d4f5abb59d..34f46d1ea0 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 @@ -15,17 +15,18 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.content.getSystemService import coil3.SingletonImageLoader import coil3.annotation.DelicateCoilApi -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.appconfig.ElementCallConfig import io.element.android.features.call.api.CallType import io.element.android.features.call.api.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.di.annotations.ApplicationContext 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 @@ -33,6 +34,7 @@ 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 io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job @@ -52,8 +54,7 @@ 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 +import kotlin.math.min /** * Manages the active call state. @@ -86,7 +87,8 @@ interface ActiveCallManager { @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultActiveCallManager @Inject constructor( +@Inject +class DefaultActiveCallManager( @ApplicationContext context: Context, @AppCoroutineScope private val coroutineScope: CoroutineScope, @@ -97,6 +99,7 @@ class DefaultActiveCallManager @Inject constructor( private val defaultCurrentCallService: DefaultCurrentCallService, private val appForegroundStateService: AppForegroundStateService, private val imageLoaderHolder: ImageLoaderHolder, + private val systemClock: SystemClock, ) : ActiveCallManager { private val tag = "DefaultActiveCallManager" private var timedOutCallJob: Job? = null @@ -117,8 +120,20 @@ class DefaultActiveCallManager @Inject constructor( override suspend fun registerIncomingCall(notificationData: CallNotificationData) { mutex.withLock { + val ringDuration = + min( + notificationData.expirationTimestamp - systemClock.epochMillis(), + ElementCallConfig.RINGING_CALL_DURATION_SECONDS * 1000L + ) + + if (ringDuration < 0) { + // Should already have stopped ringing, ignore. + Timber.tag(tag).d("Received timed-out incoming ringing call for room id: ${notificationData.roomId}, cancel ringing") + return + } + appForegroundStateService.updateHasRingingCall(true) - Timber.tag(tag).d("Received incoming call for room id: ${notificationData.roomId}") + Timber.tag(tag).d("Received incoming call for room id: ${notificationData.roomId}, ringDuration(ms): $ringDuration") if (activeCall.value != null) { displayMissedCallNotification(notificationData) Timber.tag(tag).w("Already have an active call, ignoring incoming call: $notificationData") @@ -137,14 +152,14 @@ class DefaultActiveCallManager @Inject constructor( showIncomingCallNotification(notificationData) // Wait for the ringing call to time out - delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds) + delay(timeMillis = ringDuration) 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) + activeWakeLock.acquire(ringDuration) } } } @@ -179,12 +194,22 @@ class DefaultActiveCallManager @Inject constructor( } override suspend fun hungUpCall(callType: CallType) = mutex.withLock { - if (activeCall.value?.callType != callType) { + Timber.tag(tag).d("Hung up call: $callType") + val currentActiveCall = activeCall.value ?: run { + Timber.tag(tag).w("No active call, ignoring hang up") + return + } + if (currentActiveCall.callType != callType) { Timber.tag(tag).w("Call type $callType does not match the active call type, ignoring") return } - - Timber.tag(tag).d("Hung up call: $callType") + if (currentActiveCall.callState is CallState.Ringing) { + // Decline the call + val notificationData = currentActiveCall.callState.notificationData + matrixClientProvider.getOrRestore(notificationData.sessionId).getOrNull() + ?.getRoom(notificationData.roomId) + ?.declineCall(notificationData.eventId) + } cancelIncomingCallNotification() if (activeWakeLock?.isHeld == true) { @@ -225,6 +250,7 @@ class DefaultActiveCallManager @Inject constructor( notificationChannelId = notificationData.notificationChannelId, timestamp = notificationData.timestamp, textContent = notificationData.textContent, + expirationTimestamp = notificationData.expirationTimestamp, ) ?: return runCatchingExceptions { notificationManagerCompat.notify( @@ -255,6 +281,43 @@ class DefaultActiveCallManager @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) private fun observeRingingCall() { + activeCall + .filterNotNull() + .filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall } + .flatMapLatest { activeCall -> + val callType = activeCall.callType as CallType.RoomCall + val ringingInfo = activeCall.callState as CallState.Ringing + val client = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?: run { + Timber.tag(tag).d("Couldn't find session for incoming call: $activeCall") + return@flatMapLatest flowOf() + } + val room = client.getRoom(callType.roomId) ?: run { + Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall") + return@flatMapLatest flowOf() + } + + Timber.tag(tag).d("Found room for ringing call: ${room.roomId}") + + // If we have declined from another phone we want to stop ringing. + room.subscribeToCallDecline(ringingInfo.notificationData.eventId) + .filter { decliner -> + Timber.tag(tag).d("Call: $activeCall was declined by $decliner") + // only want to listen if the call was declined from another of my sessions, + // (we are ringing for an incoming call in a DM) + decliner == client.sessionId + } + } + .onEach { decliner -> + Timber.tag(tag).d("Call: $activeCall was declined by user from another session") + // Remove the active call and cancel the notification + activeCall.value = null + if (activeWakeLock?.isHeld == true) { + Timber.tag(tag).d("Releasing partial wakelock after call declined from another session") + activeWakeLock.release() + } + cancelIncomingCallNotification() + } + .launchIn(coroutineScope) // This will observe ringing calls and ensure they're terminated if the room call is cancelled or if the user // has joined the call from another session. activeCall 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 c81b87746e..09fcfe3940 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 @@ -9,9 +9,10 @@ package io.element.android.features.call.impl.utils import android.net.Uri import androidx.core.net.toUri -import javax.inject.Inject +import dev.zacsweers.metro.Inject -class CallIntentDataParser @Inject constructor() { +@Inject +class CallIntentDataParser { private val validHttpSchemes = sequenceOf("https") private val knownHosts = sequenceOf( "call.element.io", 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 index c978aba7e8..aafb7fdde0 100644 --- 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 @@ -7,14 +7,15 @@ package io.element.android.features.call.impl.utils -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 { +@Inject +class DefaultCallAnalyticCredentialsProvider : 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() } 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 f5d50ecfe6..dc217bc118 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 @@ -7,9 +7,10 @@ package io.element.android.features.call.impl.utils -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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.RoomId import io.element.android.libraries.matrix.api.core.SessionId @@ -18,12 +19,12 @@ 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( +@Inject +class DefaultCallWidgetProvider( private val matrixClientsProvider: MatrixClientProvider, private val appPreferencesStore: AppPreferencesStore, private val callWidgetSettingsProvider: CallWidgetSettingsProvider, @@ -44,8 +45,14 @@ class DefaultCallWidgetProvider @Inject constructor( 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, direct = room.isDm()) + val roomInfo = room.info() + val isEncrypted = roomInfo.isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow() + val widgetSettings = callWidgetSettingsProvider.provide( + baseUrl = baseUrl, + encrypted = isEncrypted, + direct = room.isDm(), + hasActiveCall = roomInfo.hasRoomCall, + ) val callUrl = room.generateWidgetWebViewUrl( widgetSettings = widgetSettings, clientId = clientId, diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallService.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallService.kt index 0e561713b3..3ae65e338b 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallService.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallService.kt @@ -7,17 +7,18 @@ package io.element.android.features.call.impl.utils -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.call.api.CurrentCall import io.element.android.features.call.api.CurrentCallService -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn import kotlinx.coroutines.flow.MutableStateFlow -import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultCurrentCallService @Inject constructor() : CurrentCallService { +@Inject +class DefaultCurrentCallService : CurrentCallService { override val currentCall = MutableStateFlow(CurrentCall.None) fun onCallStarted(call: CurrentCall) { diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt index 51f758a3bb..7745cda13b 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt @@ -29,7 +29,7 @@ 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 +import kotlin.time.Duration.Companion.seconds /** * This class manages the audio devices for a WebView. @@ -246,7 +246,6 @@ class WebViewAudioManager( 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) }, @@ -254,7 +253,7 @@ class WebViewAudioManager( 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) + delay(2.seconds) // Calling this ahead of time makes the default audio device to not use the right audio stream setAvailableAudioDevices() 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 55adc246e9..82500c5937 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 @@ -133,6 +133,7 @@ class WebViewWidgetMessageInterceptor( return assetLoader.shouldInterceptRequest(request.url) } + @Suppress("OVERRIDE_DEPRECATION") override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? { return assetLoader.shouldInterceptRequest(url.toUri()) } 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 9e2ef1cea4..8fabd16ce8 100644 --- a/features/call/impl/src/main/res/values-de/translations.xml +++ b/features/call/impl/src/main/res/values-de/translations.xml @@ -3,6 +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." + "Element Call unterstützt in dieser Android-Version keine Bluetooth Geräte. Bitte wähle ein anderes Audiogerät." "Eingehender Element Call" diff --git a/features/call/impl/src/main/res/values-ko/translations.xml b/features/call/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..7900c57d6c --- /dev/null +++ b/features/call/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,8 @@ + + + "수신 중인 통화" + "탭해서 통화로 돌아가기" + "☎️ 통화 진행 중" + "Element Call은 이 Android 버전에서 Bluetooth 오디오 장치 사용을 지원하지 않습니다. 다른 오디오 장치를 선택하세요." + "Element 전화 수신" + diff --git a/features/call/impl/src/main/res/values-ro/translations.xml b/features/call/impl/src/main/res/values-ro/translations.xml index ecbccde6b4..7d619d5978 100644 --- a/features/call/impl/src/main/res/values-ro/translations.xml +++ b/features/call/impl/src/main/res/values-ro/translations.xml @@ -3,5 +3,6 @@ "Apel în curs" "Atingeți pentru a reveni la apel." "☎️ Apel în curs" + "Element Call nu permite utilizarea dispozitivelor audio Bluetooth în această versiune Android. Vă rugăm să selectați un alt dispozitiv audio." "Primiți un apel Element Call" diff --git a/features/call/impl/src/main/res/values-ru/translations.xml b/features/call/impl/src/main/res/values-ru/translations.xml index 6bf3ebbb94..e7de6d3288 100644 --- a/features/call/impl/src/main/res/values-ru/translations.xml +++ b/features/call/impl/src/main/res/values-ru/translations.xml @@ -3,5 +3,6 @@ "Текущий вызов" "Коснитесь, чтобы вернуться к вызову" "☎️ Идёт вызов" + "Функция Element Call не поддерживает использование аудиоустройств Bluetooth в данной версии Android. Пожалуйста, выберите другое аудиоустройство." "Входящий вызов Element" diff --git a/features/call/impl/src/main/res/values-uz/translations.xml b/features/call/impl/src/main/res/values-uz/translations.xml index daab8211cb..ceab664cc2 100644 --- a/features/call/impl/src/main/res/values-uz/translations.xml +++ b/features/call/impl/src/main/res/values-uz/translations.xml @@ -4,4 +4,5 @@ "Qo\'ng\'iroqqa qaytish uchun bosing" "☎️ Qoʻngʻiroq davom etmoqda" "Element Call ushbu Android versiyasida Bluetooth audio qurilmalaridan foydalanishni qoʻllab-quvvatlamaydi. Iltimos, boshqa audio qurilmani tanlang." + "Kiruvchi element qoʻngʻirogʻi" 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 86d9b80d65..7d0f15b540 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 @@ -59,6 +59,7 @@ class DefaultElementCallEntryPointTest { senderName = "senderName", avatarUrl = "avatarUrl", timestamp = 0, + expirationTimestamp = 0, notificationChannelId = "notificationChannelId", textContent = "textContent", ) 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 f36267a353..0af482fc49 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 @@ -73,6 +73,7 @@ class RingingCallNotificationCreatorTest { roomAvatarUrl = "https://example.com/avatar.jpg", notificationChannelId = "channelId", timestamp = 0L, + expirationTimestamp = 20L, textContent = "textContent", ) 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 15f1334773..47a91afa1c 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 @@ -215,7 +215,7 @@ import kotlin.time.Duration.Companion.seconds } @Test - fun `present - a received 'joined' action makes the call to be active`() = runTest { + fun `present - a received 'content loaded' action makes the call to be active`() = runTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( @@ -238,7 +238,7 @@ import kotlin.time.Duration.Companion.seconds messageInterceptor.givenInterceptedMessage( """ { - "action":"io.element.join", + "action":"content_loaded", "api":"fromWidget", "widgetId":"1", "requestId":"1" 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 84084c38fe..3d1c35df4d 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 @@ -22,13 +22,16 @@ import io.element.android.features.call.test.aCallNotificationData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId +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.AN_EVENT_ID_2 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_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.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.api.notifications.ForegroundServiceType import io.element.android.libraries.push.api.notifications.NotificationIdProvider @@ -36,8 +39,12 @@ import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolde 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.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.plantTestTimber +import io.mockk.coVerify import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -164,6 +171,102 @@ class DefaultActiveCallManagerTest { verify { notificationManagerCompat.cancel(notificationId) } } + @Test + fun `Decline event - Hangup on a ringing call should send a decline event`() = runTest { + setupShadowPowerManager() + val notificationManagerCompat = mockk(relaxed = true) + + val room = mockk(relaxed = true) + + val matrixClient = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val clientProvider = FakeMatrixClientProvider({ Result.success(matrixClient) }) + + val manager = createActiveCallManager( + matrixClientProvider = clientProvider, + notificationManagerCompat = notificationManagerCompat + ) + + val notificationData = aCallNotificationData(roomId = A_ROOM_ID) + manager.registerIncomingCall(notificationData) + + manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId)) + + coVerify { + room.declineCall(notificationEventId = notificationData.eventId) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `Decline event - Declining from another session should stop ringing`() = runTest { + setupShadowPowerManager() + val notificationManagerCompat = mockk(relaxed = true) + + val room = FakeJoinedRoom() + + val matrixClient = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val clientProvider = FakeMatrixClientProvider({ Result.success(matrixClient) }) + + val manager = createActiveCallManager( + matrixClientProvider = clientProvider, + notificationManagerCompat = notificationManagerCompat + ) + + val notificationData = aCallNotificationData(roomId = A_ROOM_ID) + manager.registerIncomingCall(notificationData) + + runCurrent() + + // Simulate declined from other session + room.baseRoom.givenDecliner(matrixClient.sessionId, notificationData.eventId) + + runCurrent() + + assertThat(manager.activeCall.value).isNull() + assertThat(manager.activeWakeLock?.isHeld).isFalse() + + verify { notificationManagerCompat.cancel(notificationId) } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `Decline event - Should ignore decline for other notification events`() = runTest { + plantTestTimber() + setupShadowPowerManager() + val notificationManagerCompat = mockk(relaxed = true) + + val room = FakeJoinedRoom() + + val matrixClient = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val clientProvider = FakeMatrixClientProvider({ Result.success(matrixClient) }) + + val manager = createActiveCallManager( + matrixClientProvider = clientProvider, + notificationManagerCompat = notificationManagerCompat + ) + + val notificationData = aCallNotificationData(roomId = A_ROOM_ID) + manager.registerIncomingCall(notificationData) + + runCurrent() + + // Simulate declined for another notification event + room.baseRoom.givenDecliner(matrixClient.sessionId, AN_EVENT_ID_2) + + runCurrent() + + assertThat(manager.activeCall.value).isNotNull() + assertThat(manager.activeWakeLock?.isHeld).isTrue() + + verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) } + } + @Test fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest { setupShadowPowerManager() @@ -267,6 +370,83 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isNotNull() } + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `IncomingCall - rings no longer than expiration time`() = runTest { + setupShadowPowerManager() + val notificationManagerCompat = mockk(relaxed = true) + val clock = FakeSystemClock() + val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat, systemClock = clock) + + assertThat(manager.activeWakeLock?.isHeld).isFalse() + assertThat(manager.activeCall.value).isNull() + + val eventTimestamp = A_FAKE_TIMESTAMP + // The call should not ring more than 30 seconds after the initial event was sent + val expirationTimestamp = eventTimestamp + 30_000 + + val callNotificationData = aCallNotificationData( + timestamp = eventTimestamp, + expirationTimestamp = expirationTimestamp, + ) + + // suppose it took 10s to be notified + clock.epochMillisResult = eventTimestamp + 10_000 + manager.registerIncomingCall(callNotificationData) + + assertThat(manager.activeCall.value).isEqualTo( + ActiveCall( + callType = CallType.RoomCall( + sessionId = callNotificationData.sessionId, + roomId = callNotificationData.roomId, + ), + callState = CallState.Ringing(callNotificationData) + ) + ) + + runCurrent() + + assertThat(manager.activeWakeLock?.isHeld).isTrue() + verify { notificationManagerCompat.notify(notificationId, any()) } + + // advance by 21s it should have stopped ringing + advanceTimeBy(21_000) + runCurrent() + + verify { notificationManagerCompat.cancel(any()) } + } + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `IncomingCall - ignore expired ring lifetime`() = runTest { + setupShadowPowerManager() + val notificationManagerCompat = mockk(relaxed = true) + val clock = FakeSystemClock() + val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat, systemClock = clock) + + assertThat(manager.activeWakeLock?.isHeld).isFalse() + assertThat(manager.activeCall.value).isNull() + + val eventTimestamp = A_FAKE_TIMESTAMP + // The call should not ring more than 30 seconds after the initial event was sent + val expirationTimestamp = eventTimestamp + 30_000 + + val callNotificationData = aCallNotificationData( + timestamp = eventTimestamp, + expirationTimestamp = expirationTimestamp, + ) + + // suppose it took 35s to be notified + clock.epochMillisResult = eventTimestamp + 35_000 + manager.registerIncomingCall(callNotificationData) + + assertThat(manager.activeCall.value).isNull() + + runCurrent() + + assertThat(manager.activeWakeLock?.isHeld).isFalse() + verify(exactly = 0) { notificationManagerCompat.notify(notificationId, any()) } + } + private fun setupShadowPowerManager() { shadowOf(InstrumentationRegistry.getInstrumentation().targetContext.getSystemService()).apply { setIsWakeLockLevelSupported(PowerManager.PARTIAL_WAKE_LOCK, true) @@ -277,6 +457,7 @@ class DefaultActiveCallManagerTest { matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(), notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true), + systemClock: FakeSystemClock = FakeSystemClock(), ) = DefaultActiveCallManager( context = InstrumentationRegistry.getInstrumentation().targetContext, coroutineScope = backgroundScope, @@ -292,5 +473,6 @@ class DefaultActiveCallManagerTest { defaultCurrentCallService = DefaultCurrentCallService(), appForegroundStateService = FakeAppForegroundStateService(), imageLoaderHolder = FakeImageLoaderHolder(), + systemClock = systemClock, ) } 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 8e6ee00a16..2c56ab299b 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, + expirationTimestamp: Long = 30_000L, textContent: String? = null, ): CallNotificationData = CallNotificationData( sessionId = sessionId, @@ -41,5 +42,6 @@ fun aCallNotificationData( avatarUrl = avatarUrl, notificationChannelId = notificationChannelId, timestamp = timestamp, + expirationTimestamp = expirationTimestamp, 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 09a1269259..cd2e617b4d 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 @@ -38,6 +38,7 @@ class FakeElementCallEntryPoint( senderName: String?, avatarUrl: String?, timestamp: Long, + expirationTimestamp: Long, notificationChannelId: String, textContent: String?, ) { diff --git a/features/changeroommemberroles/api/build.gradle.kts b/features/changeroommemberroles/api/build.gradle.kts index 9ec13286da..655bd5683e 100644 --- a/features/changeroommemberroles/api/build.gradle.kts +++ b/features/changeroommemberroles/api/build.gradle.kts @@ -1,5 +1,3 @@ -import extension.setupAnvil - /* * Copyright 2025 New Vector Ltd. * @@ -16,10 +14,7 @@ android { namespace = "io.element.android.features.changeroommemberroles.api" } -setupAnvil() - dependencies { - implementation(projects.anvilannotations) implementation(projects.libraries.architecture) implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) diff --git a/features/changeroommemberroles/api/src/main/kotlin/io/element/android/features/changeroommemberroes/api/ChangeRoomMemberRolesEntryPoint.kt b/features/changeroommemberroles/api/src/main/kotlin/io/element/android/features/changeroommemberroes/api/ChangeRoomMemberRolesEntryPoint.kt index 0175cdf247..b6f7680b38 100644 --- a/features/changeroommemberroles/api/src/main/kotlin/io/element/android/features/changeroommemberroes/api/ChangeRoomMemberRolesEntryPoint.kt +++ b/features/changeroommemberroles/api/src/main/kotlin/io/element/android/features/changeroommemberroes/api/ChangeRoomMemberRolesEntryPoint.kt @@ -14,7 +14,7 @@ import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom -interface ChangeRoomMemberRolesEntryPoint : FeatureEntryPoint { +fun interface ChangeRoomMemberRolesEntryPoint : FeatureEntryPoint { fun builder(parentNode: Node, buildContext: BuildContext): Builder interface Builder { diff --git a/features/changeroommemberroles/impl/build.gradle.kts b/features/changeroommemberroles/impl/build.gradle.kts index dfe7690aed..be2bf17979 100644 --- a/features/changeroommemberroles/impl/build.gradle.kts +++ b/features/changeroommemberroles/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2025 New Vector Ltd. @@ -22,7 +23,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.changeroommemberroles.api) @@ -37,15 +38,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.services.analytics.api) + testCommonDependencies(libs, true) testImplementation(projects.services.analytics.test) - testImplementation(libs.test.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.tests.testutils) - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesNode.kt b/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesNode.kt index 5b8a839658..1b9c790b25 100644 --- a/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesNode.kt +++ b/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesNode.kt @@ -14,9 +14,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.appyx.launchMolecule @@ -26,7 +26,8 @@ import io.element.android.libraries.matrix.api.room.RoomMember import kotlinx.coroutines.flow.first @ContributesNode(RoomScope::class) -class ChangeRolesNode @AssistedInject constructor( +@AssistedInject +class ChangeRolesNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: ChangeRolesPresenter.Factory, @@ -36,16 +37,7 @@ class ChangeRolesNode @AssistedInject constructor( ) : NodeInputs private val inputs: Inputs = inputs() - - private val presenter = presenterFactory.run { - val role = when (inputs.listType) { - ChangeRoomMemberRolesListType.Admins -> RoomMember.Role.Admin - ChangeRoomMemberRolesListType.Moderators -> RoomMember.Role.Moderator - ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving -> RoomMember.Role.Owner(isCreator = false) - } - create(role) - } - + private val presenter = presenterFactory.create(inputs.listType.toRoomMemberRole()) private val stateFlow = launchMolecule { presenter.present() } suspend fun waitForRoleChanged() { @@ -62,3 +54,9 @@ class ChangeRolesNode @AssistedInject constructor( ) } } + +internal fun ChangeRoomMemberRolesListType.toRoomMemberRole() = when (this) { + ChangeRoomMemberRolesListType.Admins -> RoomMember.Role.Admin + ChangeRoomMemberRolesListType.Moderators -> RoomMember.Role.Moderator + ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving -> RoomMember.Role.Owner(isCreator = false) +} diff --git a/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesPresenter.kt b/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesPresenter.kt index ce971464f4..95177f8377 100644 --- a/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesPresenter.kt +++ b/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesPresenter.kt @@ -18,9 +18,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.RoomModeration import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter @@ -47,14 +47,15 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -class ChangeRolesPresenter @AssistedInject constructor( +@AssistedInject +class ChangeRolesPresenter( @Assisted private val role: RoomMember.Role, private val room: JoinedRoom, private val dispatchers: CoroutineDispatchers, private val analyticsService: AnalyticsService, ) : Presenter { @AssistedFactory - interface Factory { + fun interface Factory { fun create(role: RoomMember.Role): ChangeRolesPresenter } diff --git a/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRoomMemberRolesRootNode.kt b/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRoomMemberRolesRootNode.kt index 881e93e934..2c3f77f208 100644 --- a/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRoomMemberRolesRootNode.kt +++ b/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRoomMemberRolesRootNode.kt @@ -16,38 +16,36 @@ import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode 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.appnav.di.RoomComponentFactory +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.appnav.di.RoomGraphFactory import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.DaggerComponentOwner +import io.element.android.libraries.di.DependencyInjectionGraphOwner import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -class ChangeRoomMemberRolesRootNode @AssistedInject constructor( +@AssistedInject +class ChangeRoomMemberRolesRootNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - roomComponentFactory: RoomComponentFactory, + roomGraphFactory: RoomGraphFactory, ) : ParentNode( navModel = PermanentNavModel( - navTargets = setOf(NavTarget.Root), + navTargets = setOf(NavTarget), savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, plugins = plugins, -), DaggerComponentOwner, ChangeRoomMemberRolesEntryPoint.NodeProxy { - sealed interface NavTarget : Parcelable { - @Parcelize - object Root : NavTarget - } +), DependencyInjectionGraphOwner, ChangeRoomMemberRolesEntryPoint.NodeProxy { + @Parcelize object NavTarget : Parcelable data class Inputs( val joinedRoom: JoinedRoom, @@ -56,17 +54,13 @@ class ChangeRoomMemberRolesRootNode @AssistedInject constructor( private val inputs = inputs() - override val daggerComponent = roomComponentFactory.create(inputs.joinedRoom) + override val graph = roomGraphFactory.create(inputs.joinedRoom) override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { - return when (navTarget) { - NavTarget.Root -> { - createNode( - buildContext = buildContext, - plugins = listOf(ChangeRolesNode.Inputs(listType = inputs.listType)), - ) - } - } + return createNode( + buildContext = buildContext, + plugins = listOf(ChangeRolesNode.Inputs(listType = inputs.listType)), + ) } @Composable diff --git a/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/DefaultChangeRoomMemberRolesEntyPoint.kt b/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/DefaultChangeRoomMemberRolesEntyPoint.kt index 4dc26cde8e..8a9117776d 100644 --- a/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/DefaultChangeRoomMemberRolesEntyPoint.kt +++ b/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/DefaultChangeRoomMemberRolesEntyPoint.kt @@ -9,16 +9,17 @@ package io.element.android.features.changeroommemberroles.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.room.JoinedRoom -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultChangeRoomMemberRolesEntyPoint @Inject constructor() : ChangeRoomMemberRolesEntryPoint { +@Inject +class DefaultChangeRoomMemberRolesEntyPoint : ChangeRoomMemberRolesEntryPoint { override fun builder(parentNode: Node, buildContext: BuildContext): ChangeRoomMemberRolesEntryPoint.Builder { return object : ChangeRoomMemberRolesEntryPoint.Builder { private lateinit var changeRoomMemberRolesListType: ChangeRoomMemberRolesListType diff --git a/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/RoomMemberListDataSource.kt b/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/RoomMemberListDataSource.kt index 184a7058c7..a27833122a 100644 --- a/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/RoomMemberListDataSource.kt +++ b/features/changeroommemberroles/impl/src/main/kotlin/io/element/android/features/changeroommemberroles/impl/RoomMemberListDataSource.kt @@ -7,15 +7,16 @@ package io.element.android.features.changeroommemberroles.impl +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers 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( +@Inject +class RoomMemberListDataSource( private val room: BaseRoom, private val coroutineDispatchers: CoroutineDispatchers, ) { diff --git a/features/changeroommemberroles/impl/src/main/res/values-cs/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-cs/translations.xml index 504b88b23c..a66795ea7e 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-cs/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-cs/translations.xml @@ -17,13 +17,17 @@ "Upravit správce" "Tuto akci nebudete moci vrátit zpět. Upravujete oprávnění uživatele, tak aby měl stejnou úroveň jako vy." "Přidat správce?" + "Tuto akci nebudete moci vrátit zpět. Převádíte vlastnictví na vybrané uživatele. Jakmile tuto akci opustíte, bude tato změna trvalá." + "Převést vlastnictví?" "Degradovat" "Tuto změnu nebudete moci vrátit zpět, protože sami degradujete, pokud jste posledním privilegovaným uživatelem v místnosti, nebude možné znovu získat oprávnění." "Degradovat se?" "%1$s (čekající)" "(Čeká na vyřízení)" "Správci mají automaticky oprávnění moderátora" + "Vlastníci mají automaticky administrátorská oprávnění." "Upravit moderátory" + "Vyberte vlastníky" "Správci" "Moderátoři" "Členové" @@ -49,12 +53,14 @@ "Členové místnosti" "Rušení vykázání %1$s" "Správci" + "Správci a vlastníci" "Změnit moji roli" "Degradovat na člena" "Degradovat na moderátora" "Moderování členů" "Zprávy a obsah" "Moderátoři" + "Vlastníci" "Oprávnění" "Obnovit oprávnění" "Po obnovení oprávnění ztratíte aktuální nastavení." diff --git a/features/changeroommemberroles/impl/src/main/res/values-cy/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-cy/translations.xml index 53d4927f0f..4740c15888 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-cy/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-cy/translations.xml @@ -2,7 +2,7 @@ "Gweinyddwyr yn unig" "Gwahardd pobl" - "Dileu negeseuon" + "Tynnu negeseuon" "Pawb" "Gwahodd pobl a derbyn ceisiadau i ymuno" "Cymedroli aelodau" @@ -17,13 +17,17 @@ "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?" + "Fyddwch chi ddim yn gallu dadwneud y weithred hon. Rydych yn trosglwyddo\'r berchnogaeth i\'r defnyddwyr a ddewiswyd. Unwaith y byddwch yn gadael bydd hyn yn barhaol." + "Trosglwyddo perchnogaeth?" "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" + "Mae gan berchnogion freintiau gweinyddwr yn awtomatig." "Golygu Cymedrolwyr" + "Dewiswch Berchnogion" "Gweinyddwyr" "Cymedrolwyr" "Aelodau" @@ -48,15 +52,18 @@ "Dan ystyriaeth" "Gweinyddwr" "Cymedrolwr" + "Perchennog" "Aelodau\'r ystafell" "Dad-wahardd %1$s" "Gweinyddwyr" + "Gweinyddwyr a pherchnogion" "Newid fy rôl" "Israddio aelod" "Israddio cymedrolwr" "Cymedroli aelodau" "Negeseuon a chynnwys" "Cymedrolwyr" + "Perchnogion" "Caniatâd" "Ailosod caniatâd" "Ar ôl i chi ailosod caniatâd, byddwch yn colli\'r gosodiadau cyfredol." diff --git a/features/changeroommemberroles/impl/src/main/res/values-da/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-da/translations.xml index a1b2bf6e05..122453e121 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-da/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-da/translations.xml @@ -1,7 +1,7 @@ "Kun admins" - "Spær personer" + "Spær brugere" "Fjern beskeder" "Alle" "Invitér personer og acceptér anmodninger om at deltage" diff --git a/features/changeroommemberroles/impl/src/main/res/values-de/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-de/translations.xml index 93a36500e3..88d795d614 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-de/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-de/translations.xml @@ -1,35 +1,39 @@ - "Nur Administratoren" + "Nur Admins" "Mitglieder sperren" - "Nachrichten anderer Mitgliedern löschen" + "Nachrichten entfernen" "Alle" - "Leute einladen und Beitrittsanfragen annehmen" + "Personen einladen und Beitrittsanfragen annehmen" "Moderation der Mitglieder" "Nachrichten senden & löschen" - "Administratoren und Moderatoren" + "Admins und Moderatoren" "Personen entfernen und Beitrittsanfragen ablehnen" "Avatar ändern" - "Raum-Details anpassen" - "Raumname ändern" - "Raumthema ändern" + "Chat-Details anpassen" + "Chat-Namen ändern" + "Chat Thema ändern" "Nachrichten senden" "Admins bearbeiten" - "Sie können diese Aktion nicht mehr rückgängig machen. Sie vergeben die gleiche Rolle, die Sie auch haben." - "Als Administrator hinzufügen?" + "Du kannst diese Aktion nicht mehr rückgängig machen. Du vergibst dieselbe Rolle, die du auch hast." + "Als Admin hinzufügen?" + "Du kannst diese Aktion nicht rückgängig machen. Du überträgst die Eigentumsrechte an die ausgewählten Nutzer. Sobald du diesen Vorgang abschließt, ist er endgültig." + "Eigentumsrechte übertragen?" "Zurückstufen" - "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?" + "Du stufst dich selbst herab. Diese Änderung kann nicht rückgängig gemacht werden. Wenn du der letzte Nutzer mit dieser Rolle bist, ist es nicht möglich, diese Rolle wiederzuerlangen." + "Möchtest du dich selbst herabstufen?" "%1$s (Ausstehend)" "(Ausstehend)" - "Administratoren haben automatisch Moderatorenrechte" + "Admins haben automatisch Moderatorenrechte" + "Eigentümer haben automatisch Adminrechte." "Moderatoren bearbeiten" - "Administratoren" + "Wähle Eigentümer" + "Admins" "Moderatoren" "Mitglieder" - "Sie haben ungespeicherte Änderungen." + "Du hast nicht gespeicherte Änderungen." "Änderungen speichern?" - "In diesem Chatroom gibt es keine gesperrten Nutzer." + "In diesem Chat gibt es keine gesperrten Nutzer." "%1$d Person" "%1$d Personen" @@ -37,27 +41,30 @@ "Mitglied entfernen und sperren" "Mitglied nur entfernen" "Sperre aufheben" - "Die Nutzer können den Raum wieder beitreten, wenn sie dazu eingeladen werden." + "Die Nutzer können dem Chat wieder beitreten, wenn sie eingeladen werden." "Nutzer entsperren" "Gesperrt" "Mitglieder" "Ausstehend" - "Administrator" + "Admin" "Moderator" + "Eigentümer" "Mitglieder" "%1$s wird entsperrt." - "Administratoren" + "Admins" + "Admins und Eigentümer" "Ändere meine Rolle" "Zum Mitglied herabstufen" "Zum Moderator herabstufen" "Moderation der Mitglieder" "Nachrichten senden & löschen" "Moderatoren" + "Eigentümer" "Berechtigungen" "Rollen und Berechtigungen zurücksetzen" - "Sobald Sie die Berechtigungen zurücksetzen, verlieren Sie die aktuellen Einstellungen." + "Sobald du die Berechtigungen zurücksetzt, verlierst du die aktuellen Einstellungen." "Berechtigungen zurücksetzen?" "Rollen" - "Raum-Details anpassen" + "Chat-Details anpassen" "Rollen und Berechtigungen" diff --git a/features/changeroommemberroles/impl/src/main/res/values-et/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-et/translations.xml index 044503f1de..a43a9a89d0 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-et/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-et/translations.xml @@ -2,7 +2,7 @@ "Vaid peakasutajad" "Suhtluskeelu seadmine" - "Sõnumite kustutamine" + "Eemalda sõnumid" "Kõik" "Kutsu teisi osalejaid ja vasta ise liitumiskutsetele" "Jututoas osalejate modereerimine" diff --git a/features/changeroommemberroles/impl/src/main/res/values-fi/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-fi/translations.xml index b8ff96085a..1279d466e2 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-fi/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-fi/translations.xml @@ -16,12 +16,12 @@ "Viestien lähettäminen" "Muokkaa ylläpitäjiä" "Et voi peruuttaa tätä toimenpidettä. Ylennät käyttäjän samalle oikeustasolle kuin sinä." - "Lisää ylläpitäjä?" + "Lisätäänkö ylläpitäjä?" "Et voi kumota tätä toimintoa. Olet siirtämässä omistajuuden valituille käyttäjille. Kun poistut, muutos on pysyvä." "Siirretäänkö omistajuus?" "Alenna" "Et voi perua tätä muutosta, koska olet alentamassa itseäsi. Jos olet viimeinen oikeutettu henkilö tässä huoneessa, oikeuksia ei voi enää saada takaisin." - "Alenna itsesi?" + "Haluatko alentaa itsesi?" "%1$s (Kutsuttu)" "(Kutsuttu)" "Ylläpitäjillä on automaattisesti valvojan oikeudet" @@ -32,7 +32,7 @@ "Valvojat" "Jäsenet" "Sinulla on tallentamattomia muutoksia" - "Tallenna muutokset?" + "Tallennetaanko muutokset?" "Tässä huoneessa ei ole porttikieltoja" "%1$d henkilö" diff --git a/features/changeroommemberroles/impl/src/main/res/values-in/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-in/translations.xml index 1f25e9ada3..3bc68c154e 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-in/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-in/translations.xml @@ -2,7 +2,7 @@ "Hanya admin" "Cekal orang-orang" - "Hapus pesan" + "Hilangkan pesan" "Semua orang" "Undang orang-orang dan terima permintaan untuk bergabung" "Moderasi anggota" diff --git a/features/changeroommemberroles/impl/src/main/res/values-it/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-it/translations.xml index ebe1c391bf..c6ea02ebb8 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-it/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-it/translations.xml @@ -17,13 +17,17 @@ "Modifica amministratori" "Non potrai annullare questa azione. Stai promuovendo l\'utente al tuo stesso livello di potere." "Aggiungi amministratore?" + "Non potrai annullare questa azione. Stai trasferendo la proprietà agli utenti selezionati. Una volta abbandonato, questa azione sarà definitiva." + "Trasferire proprietà?" "Declassa" "Non potrai annullare questa modifica perché ti stai declassando, se sei l\'ultimo utente privilegiato nella stanza, sarà impossibile riottenere i privilegi." "Declassare te stesso?" "%1$s (In attesa)" "(In attesa)" "Gli amministratori hanno automaticamente i privilegi di moderatore" + "I proprietari hanno automaticamente privilegi di amministratore." "Modifica moderatori" + "Scegli i proprietari" "Amministratori" "Moderatori" "Membri" @@ -31,7 +35,7 @@ "Salvare le modifiche?" "Non ci sono utenti esclusi in questa stanza." - "1 persona" + "%1$d persona" "%1$d persone" "Rimuovi ed escludi" @@ -44,15 +48,18 @@ "In attesa" "Amministratore" "Moderatore" + "Proprietario" "Membri della stanza" "Riammissione di %1$s" "Amministratori" + "Amministratori e proprietari" "Cambia il mio ruolo" "Declassa a membro" "Declassa a moderatore" "Moderazione dei membri" "Messaggi e contenuti" "Moderatori" + "Proprietari" "Autorizzazioni" "Reimpostare le autorizzazioni" "Una volta reimpostate le autorizzazioni, perderai le impostazioni correnti." diff --git a/features/changeroommemberroles/impl/src/main/res/values-ko/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..fd9d38664c --- /dev/null +++ b/features/changeroommemberroles/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,69 @@ + + + "관리자 전용" + "사용자 차단" + "메시지 삭제" + "모두" + "사람들을 초대하고 가입 요청을 수락합니다" + "회원 조정" + "메시지 및 콘텐츠" + "관리자 및 중재자" + "사람들을 제거하고 가입 요청을 거부합니다" + "방 아바타 변경" + "방 세부 정보" + "방 이름 변경" + "방 화제 변경" + "메시지 보내기" + "관리자 편집" + "이 작업은 실행 취소할 수 없습니다. 해당 사용자에게 당신과 동일한 권한 레벨을 부여하는 것입니다." + "관리자를 추가하시겠습니까?" + "이 작업을 취소할 수 없습니다. 선택한 사용자에게 소유권을 이전합니다. 이 작업을 완료하면 변경 사항은 영구적으로 적용됩니다." + "소유권을 이전하시겠습니까?" + "강등하다" + "이 변경 사항은 자신을 강등하는 것이므로 실행 취소할 수 없습니다. 해당 방에서 권한을 가진 마지막 사용자인 경우 권한을 다시 얻는 것은 불가능합니다." + "자신을 강등하시겠습니까?" + "%1$s (보류 중)" + "(보류 중)" + "관리자는 자동으로 중재자 권한을 갖습니다." + "소유자는 자동으로 관리자 권한을 갖습니다." + "편집 중재자" + "소유자 선택" + "관리자" + "중재자" + "회원들" + "저장되지 않은 변경 사항이 있습니다." + "변경 사항을 저장하시겠습니까?" + "이 방에는 차단된 사용자가 없습니다." + + "%1$d 사람" + + "방에서 차단" + "회원만 삭제할 수 있습니다." + "금지 해제" + "초대받으면 이 방에 다시 들어올 수 있습니다." + "사용자 차단 해제" + "차단됨" + "회원들" + "보류 중" + "관리자" + "중재자" + "소유자" + "방 회원들" + "차단 해제 %1$s" + "관리자" + "관리자 및 소유자" + "내 역할 변경" + "회원으로 강등" + "중재자로 강등시키다" + "회원 조정" + "메시지 및 콘텐츠" + "중재자" + "소유자" + "권한" + "권한 재설정" + "권한을 재설정하면 현재 설정이 모두 삭제됩니다." + "권한을 재설정하시겠습니까?" + "역할" + "방 세부 정보" + "역할 및 권한" + diff --git a/features/changeroommemberroles/impl/src/main/res/values-nb/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-nb/translations.xml index 62f96eec72..33c9fabe24 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-nb/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-nb/translations.xml @@ -17,13 +17,16 @@ "Rediger administratorer" "Du vil ikke kunne angre denne handlingen. Du forfremmer brukeren til å ha samme rettighetsnivå som deg." "Legg til administrator?" + "Overføre eierskapet?" "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" + "Eiere har automatisk administratorrettigheter." "Rediger moderatorer" + "Velg eiere" "Administratorer" "Moderatorer" "Medlemmer" @@ -44,15 +47,18 @@ "Venter" "Administrator" "Moderator" + "Eier" "Medlemmer av rommet" "Oppheve utestengelsen av %1$s" "Administratorer" + "Administratorer og eiere" "Endre rollen min" "Nedgradere til medlem" "Nedgradere til moderator" "Moderering av medlemmer" "Meldinger og innhold" "Moderatorer" + "Eiere" "Tillatelser" "Tilbakestill tillatelser" "Når du har tilbakestilt tillatelsene, mister du gjeldende innstillinger." diff --git a/features/changeroommemberroles/impl/src/main/res/values-pt-rBR/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-pt-rBR/translations.xml index e2155955d6..6d062e5a20 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-pt-rBR/translations.xml @@ -17,13 +17,17 @@ "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)" + "Você não poderá desfazer isto. Você está transferindo a posse desta sala para os usuários selecionados. Ao sair, isto será permanente." + "Transferir posse?" + "Rebaixar" + "Você não poderá desfazer essa alteração, pois estará removendo seus próprios privilégios. Se você for o último usuário privilegiado na sala, será impossível recuperar os privilégios." + "Rebaixar seu próprio privilégio?" + "%1$s (pendente)" + "(pendente)" "Os administradores têm privilégios de moderador automaticamente" + "Proprietários automaticamente têm privilégios de administradores." "Editar moderadores" + "Escolher Proprietários" "Administradores" "Moderadores" "Membros" @@ -34,25 +38,28 @@ "%1$d pessoa" "%1$d pessoas" - "Remover e banir membro" - "Somente remover membro" + "Banir da sala" + "Somente remover o membro" "Desbanir" - "Eles poderão entrar nesta sala novamente se forem convidados." + "Esta pessoa poderá entrar nesta sala novamente se for convidada." "Desbanir usuário" "Banidos" "Membros" "Pendente" "Administrador" "Moderador" + "Proprietário" "Membros da sala" "Desbanindo %1$s" "Administradores" + "Administradores e proprietários" "Alterar meu cargo" "Rebaixar para membro" "Rebaixar para moderador" "Moderação de membros" "Mensagens e conteúdo" "Moderadores" + "Proprietários" "Permissões" "Redefinir permissões" "Depois de redefinir as permissões, você perderá as configurações atuais." diff --git a/features/changeroommemberroles/impl/src/main/res/values-ro/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-ro/translations.xml index be944409a1..c9e20f5a42 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-ro/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-ro/translations.xml @@ -2,13 +2,13 @@ "Doar administratori" "Interziceți persoane" - "Eliminați mesaje" + "Ștergeți mesajele" "Toți" - "Invitați persoane" + "Invitați persoane și acceptați cereri de alaturare" "Moderarea membrilor" "Mesaje și conținut" "Administratori și moderatori" - "Îndepărtați persoane" + "Îndepărtați persoane și refuzați cereri de alăturare" "Schimbați avatarul camerei" "Detaliile camerei" "Schimbă numele camerei" @@ -17,13 +17,17 @@ "Editați administratorii" "Promovați utilizatorul să aibă același nivel de putere ca dumneavoastră. Nu veți putea anula această acțiune." "Adăugați administrator?" + "Nu veți putea anula această acțiune. Transferați dreptul de proprietate către utilizatorii selectați. Odată ce părăsiți această pagină, acțiunea va fi definitivă." + "Transferați proprietatea?" "Retrogradare" "Nu veți putea anula această modificare, deoarece vă retrogradați. Dacă sunteți ultimul utilizator privilegiat din cameră, va fi imposibil să recâștigați privilegiile." "Vreți să vă retrogradați?" "%1$s (În așteptare)" "(În așteptare)" "Administratorii au automat privilegii de moderator" + "Proprietarii au automat privilegii de administrator." "Editați moderatorii" + "Alegeți proprietari" "Administratori" "Moderatori" "Membri" @@ -34,7 +38,7 @@ "o persoană" "%1$d persoane" - "Eliminați și interziceți membrul" + "Îndepărtați și interziceți membrul" "Doar înlăturare" "Anulare excludere" "Se vor putea alătura din nou acestei săli dacă sunt invitați." @@ -44,15 +48,18 @@ "În așteptare" "Administrator" "Moderator" + "Proprietar" "Membrii camerei" "Se anulează interzicerea lui %1$s" "Administratori" + "Administratori și proprietari" "Schimbare rol" "Degradare la membru" "Degradare la moderator" "Moderarea membrilor" "Mesaje și conținut" "Moderatori" + "Proprietari" "Permisiuni" "Resetați permisiunile" "După ce resetați permisiunile, veți pierde setările curente." diff --git a/features/changeroommemberroles/impl/src/main/res/values-ru/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-ru/translations.xml index adf745c8cf..8f5e30433d 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-ru/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-ru/translations.xml @@ -2,13 +2,13 @@ "Только администраторы" "Блокировать людей могут" - "Удалять сообщения могут" + "Удалить сообщения" "Все" - "Приглашайте людей и принимайте заявки на присоединение" + "Приглашать людей и принимать запросы на присоединение могут" "Модерация участников" "Сообщения и содержание" "Администраторы и модераторы" - "Удаляйте пользователей и отклоняйте запросы на присоединение" + "Удалять людей и отклонять запросы на присоединение могут" "Менять изображение комнаты могут" "Информация о комнате" "Менять название комнаты могут" @@ -17,13 +17,17 @@ "Редактировать роль администраторов" "Вы не сможете отменить это действие. Вы устанавливаете уровень пользователю соответствующий вашему." "Добавить администратора?" + "Отменить данное действие будет невозможно. Владение передастся выбранным пользователям. После вашего выхода действие станет необратимым." + "Передать владение?" "Понизить уровень" "Вы не сможете отменить это изменение, так как понижаете себя статус. Если вы являетесь последним привилегированным пользователем в комнате, восстановить привилегии будет невозможно." "Понизить свой уровень?" "%1$s (Ожидание)" "(В ожидании)" "Администраторы автоматически получают права модератора" + "Владельцы автоматически получают права администратора." "Редактировать роль модераторов" + "Назначить владельцев" "Администраторы" "Модераторы" "Участники" @@ -45,15 +49,18 @@ "В ожидании" "Администратор" "Модератор" + "Владелец" "Участники комнаты" "Разблокировка %1$s" "Администраторы" + "Администраторы и владельцы" "Изменить мою роль" "Понизить до участника" "Понизить до модератора" "Модерация участников" "Сообщения и содержание" "Модераторы" + "Владельцы" "Разрешения" "Сбросить разрешения" "Как только вы сбросите разрешения, все текущие настройки будут утеряны." diff --git a/features/changeroommemberroles/impl/src/main/res/values-sv/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-sv/translations.xml index 9153ccc89f..a94b7384fa 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-sv/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-sv/translations.xml @@ -17,13 +17,17 @@ "Redigera administratörer" "Du kommer inte att kunna ångra den här åtgärden. Du befordrar användaren till att ha samma behörighetsnivå som du." "Lägg till Admin?" + "Du kommer inte att kunna ångra den här åtgärden. Du överför ägarskapet till de valda användarna. När du lämnar kommer detta att vara permanent." + "Överför ägarskap?" "Degradera" "Du kommer inte att kunna ångra denna ändring eftersom du degraderar dig själv, om du är den sista privilegierade användaren i rummet kommer det att vara omöjligt att återfå privilegier." "Degradera dig själv?" "%1$s (Väntar)" "(Väntar)" "Administratörer har automatiskt moderatorbehörighet" + "Ägare har automatiskt administratörsbehörighet." "Redigera moderatorer" + "Välj ägare" "Administratörer" "Moderatorer" "Medlemmar" @@ -44,15 +48,18 @@ "Väntar" "Admin" "Moderator" + "Ägare" "Rumsmedlemmar" "Avbannar %1$s" "Administratörer" + "Administratörer och ägare" "Ändra min roll" "Degradera till medlem" "Degradera till moderator" "Medlemsmoderering" "Meddelanden och innehåll" "Moderatorer" + "Ägare" "Behörigheter" "Återställ behörigheter" "När du har återställt behörigheterna kommer du att förlora de aktuella inställningarna." diff --git a/features/changeroommemberroles/impl/src/main/res/values-uz/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-uz/translations.xml index 29df30f421..7d3d6d5727 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-uz/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-uz/translations.xml @@ -1,10 +1,63 @@ + "Faqat adminlar" + "Odamlarni taqiqlash" + "Xabarlarni olib tashlash" "Har kim" + "Odamlarni taklif qiling va qo‘shilish so‘rovlarini qabul qiling" + "Aʻzo moderatsiyasi" + "Xabarlar va kontent" + "Adminlar va moderatorlar" + "Odamlarni olib tashlash va qoʻshilish soʻrovlarini rad etish" + "Xona avatarini oʻzgartirish" + "Xona tafsilotlari" + "Xona nomini oʻzgartirish" + "Xona mavzusini almashtirish" + "Xabarlar yuborish" + "Administratorlarni tahrirlash" + "Bu amalni bekor qila olmaysiz. Siz foydalanuvchini o‘zingiz bilan bir xil quvvat darajasiga ega bo‘lishga undayapsiz." + "Admin qo‘shilsinmi?" + "Pastga tushirish" + "Siz oʻzingizni imtiyozlardan mahrum qilayotganingiz sababli, bu o‘zgarishni bekor qila olmaysiz. Agar xonadagi so‘nggi imtiyozli foydalanuvchi bo‘lsangiz, imtiyozlarni qayta tiklash imkonsiz bo‘ladi." + "O‘z darajangizni pasaytirmoqchimisiz?" + "%1$s (Jarayonda)" + "(Kutilmoqda)" + "Administratorlar avtomatik ravishda moderator imtiyozlariga ega" + "Moderatorlarni tahrirlash" + "Adminlar" + "Moderatorlar" + "Azolar" + "Sizda saqlanmagan oʻzgarishlar bor" + "O‘zgartirishlarni saqlaysizmi?" + "Bu xonada taqiqlangan foydalanuvchilar yoʻq." "%1$dodam" "%1$dodamlar" + "Xonadan chetlashtirish" + "Faqat aʻzoni olib tashlash" + "Taqiqni bekor qilish" + "Agar taklif qilinsa, ular bu xonaga qayta qo‘shilishlari mumkin." + "Foydalanuvchini blokdan chiqarish" + "Taqiqlangan" + "Azolar" "Kutilmoqda" + "Admin" + "Moderator" "Xona a\'zolari" + "Taqiqni bekor qilish %1$s" + "Adminlar" + "Rolimni o‘zgartirish" + "Aʼzolikka tushirish" + "Moderatorga pasaytirish" + "Aʻzo moderatsiyasi" + "Xabarlar va kontent" + "Moderatorlar" + "Ruxsatlar" + "Ruxsatlarni tiklash" + "Ruxsatlarni asliga qaytargach, joriy sozlamalarni yoʻqotasiz." + "Ruxsatlar asliga qaytarilsinmi?" + "Rollar" + "Xona tafsilotlari" + "Rollar va ruxsatlar" diff --git a/features/changeroommemberroles/impl/src/main/res/values-zh/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-zh/translations.xml index db1481b4d4..eeea0a7b35 100644 --- a/features/changeroommemberroles/impl/src/main/res/values-zh/translations.xml +++ b/features/changeroommemberroles/impl/src/main/res/values-zh/translations.xml @@ -17,13 +17,17 @@ "编辑管理员" "您将无法撤消此操作。您正在提升用户的权限,使其拥有与您平权。" "添加管理员?" + "此操作无法撤销。您正在将所有权转移给所选用户。一旦离开此界面,该操作将永久生效。" + "转让所有权" "降级" "您正在降级,此更改将无法撤消。如果您是聊天室中的最后一个特权用户,则无法重新获得权限。" "降级自己?" "%1$s(待处理)" "(已邀请)" "管理员自动拥有协管员权限" + "所有者自动拥有管理员权限。" "编辑协管员" + "选择所有者" "管理员" "协管员" "成员" @@ -43,15 +47,18 @@ "待处理" "管理员" "协管员" + "所有者" "聊天室成员" "解除封禁 %1$s" "管理员" + "管理员和所有者" "更改我的角色" "降级为成员" "降级为协管员" "成员权限" "消息和内容" "协管员" + "所有者" "权限" "重置权限" "重置权限后,您将丢失当前设置。" diff --git a/features/changeroommemberroles/impl/src/test/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesNodeTest.kt b/features/changeroommemberroles/impl/src/test/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesNodeTest.kt new file mode 100644 index 0000000000..5a47f52e89 --- /dev/null +++ b/features/changeroommemberroles/impl/src/test/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesNodeTest.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.changeroommemberroles.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType +import io.element.android.libraries.matrix.api.room.RoomMember +import org.junit.Test + +class ChangeRolesNodeTest { + @Test + fun `test toRoomMemberRole`() { + assertThat(ChangeRoomMemberRolesListType.Admins.toRoomMemberRole()) + .isEqualTo(RoomMember.Role.Admin) + assertThat(ChangeRoomMemberRolesListType.Moderators.toRoomMemberRole()) + .isEqualTo(RoomMember.Role.Moderator) + assertThat(ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving.toRoomMemberRole()) + .isEqualTo(RoomMember.Role.Owner(false)) + } +} diff --git a/features/changeroommemberroles/impl/src/test/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesPresenterTest.kt b/features/changeroommemberroles/impl/src/test/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesPresenterTest.kt index a5b2da566c..e5e22b8846 100644 --- a/features/changeroommemberroles/impl/src/test/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesPresenterTest.kt +++ b/features/changeroommemberroles/impl/src/test/kotlin/io/element/android/features/changeroommemberroles/impl/ChangeRolesPresenterTest.kt @@ -37,7 +37,6 @@ import kotlinx.collections.immutable.toPersistentMap import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test -import kotlin.collections.plus class ChangeRolesPresenterTest { @Test @@ -556,18 +555,18 @@ class ChangeRolesPresenterTest { users = pairs.associate { (userId, role) -> userId to role.powerLevel }.toPersistentMap() ) } - - private fun TestScope.createChangeRolesPresenter( - role: RoomMember.Role = RoomMember.Role.Admin, - room: FakeJoinedRoom = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})), - dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), - analyticsService: FakeAnalyticsService = FakeAnalyticsService(), - ): ChangeRolesPresenter { - return ChangeRolesPresenter( - role = role, - room = room, - dispatchers = dispatchers, - analyticsService = analyticsService, - ) - } +} + +internal fun TestScope.createChangeRolesPresenter( + role: RoomMember.Role = RoomMember.Role.Admin, + room: FakeJoinedRoom = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})), + dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), + analyticsService: FakeAnalyticsService = FakeAnalyticsService(), +): ChangeRolesPresenter { + return ChangeRolesPresenter( + role = role, + room = room, + dispatchers = dispatchers, + analyticsService = analyticsService, + ) } diff --git a/features/changeroommemberroles/impl/src/test/kotlin/io/element/android/features/changeroommemberroles/impl/DefaultChangeRoomMemberRolesEntyPointTest.kt b/features/changeroommemberroles/impl/src/test/kotlin/io/element/android/features/changeroommemberroles/impl/DefaultChangeRoomMemberRolesEntyPointTest.kt new file mode 100644 index 0000000000..621af8edaf --- /dev/null +++ b/features/changeroommemberroles/impl/src/test/kotlin/io/element/android/features/changeroommemberroles/impl/DefaultChangeRoomMemberRolesEntyPointTest.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.changeroommemberroles.impl + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.tests.testutils.node.TestParentNode +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DefaultChangeRoomMemberRolesEntyPointTest { + @Test + fun `test node builder`() = runTest { + val entryPoint = DefaultChangeRoomMemberRolesEntyPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + ChangeRoomMemberRolesRootNode( + buildContext = buildContext, + plugins = plugins, + roomGraphFactory = { }, + ) + } + val room = FakeJoinedRoom() + val listType = ChangeRoomMemberRolesListType.Admins + val result = entryPoint.builder(parentNode, BuildContext.root(null)) + .room(FakeJoinedRoom()) + .listType(listType) + .build() + assertThat(result).isInstanceOf(ChangeRoomMemberRolesRootNode::class.java) + // Search for the Inputs plugin + val input = result.plugins.filterIsInstance().single() + assertThat(input.joinedRoom.roomId).isEqualTo(room.roomId) + assertThat(input.listType).isEqualTo(listType) + } +} diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 99e8fc653f..712ce110f8 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -22,7 +23,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.core) @@ -32,7 +33,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) implementation(projects.libraries.androidutils) - implementation(projects.libraries.deeplink) + implementation(projects.libraries.deeplink.api) implementation(projects.libraries.mediapickers.api) implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.permissions.api) @@ -43,13 +44,7 @@ dependencies { implementation(projects.features.invitepeople.api) api(projects.features.createroom.api) - testImplementation(libs.test.junit) - testImplementation(libs.test.mockk) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs, true) testImplementation(projects.services.analytics.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.mediapickers.test) @@ -58,7 +53,4 @@ dependencies { testImplementation(projects.libraries.usersearch.test) testImplementation(projects.features.startchat.test) testImplementation(projects.libraries.featureflag.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } 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 6c819edf5a..8f46103ba5 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 @@ -16,9 +16,9 @@ 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.replace -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.createroom.impl.addpeople.AddPeopleNode import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode @@ -30,7 +30,8 @@ import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -class CreateRoomFlowNode @AssistedInject constructor( +@AssistedInject +class CreateRoomFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, ) : BaseFlowNode( diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt index b9dfb20960..0d62542504 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt @@ -10,14 +10,15 @@ package io.element.android.features.createroom.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 dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultCreateRoomEntryPoint @Inject constructor() : CreateRoomEntryPoint { +@Inject +class DefaultCreateRoomEntryPoint : CreateRoomEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreateRoomEntryPoint.NodeBuilder { val plugins = ArrayList() diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt index 3fc9cd7d81..9ea89912cb 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt @@ -13,9 +13,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.invitepeople.api.InvitePeoplePresenter import io.element.android.features.invitepeople.api.InvitePeopleRenderer import io.element.android.libraries.architecture.NodeInputs @@ -24,7 +24,8 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId @ContributesNode(SessionScope::class) -class AddPeopleNode @AssistedInject constructor( +@AssistedInject +class AddPeopleNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, invitePeoplePresenterFactory: InvitePeoplePresenter.Factory, 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 6927c51366..9e721d28bd 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 @@ -14,16 +14,17 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(SessionScope::class) -class ConfigureRoomNode @AssistedInject constructor( +@AssistedInject +class ConfigureRoomNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: ConfigureRoomPresenter, 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 9b2880fb6f..29b9842abe 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 @@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.CreatedRoom import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter @@ -44,10 +45,10 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject import kotlin.jvm.optionals.getOrDefault -class ConfigureRoomPresenter @Inject constructor( +@Inject +class ConfigureRoomPresenter( private val dataStore: CreateRoomConfigStore, private val matrixClient: MatrixClient, private val mediaPickerProvider: PickerProvider, diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfigStore.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfigStore.kt index bee9b686d6..f2701216c3 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfigStore.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfigStore.kt @@ -8,15 +8,16 @@ package io.element.android.features.createroom.impl.configureroom import android.net.Uri +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.getAndUpdate import java.io.File -import javax.inject.Inject -class CreateRoomConfigStore @Inject constructor( +@Inject +class CreateRoomConfigStore( private val roomAliasHelper: RoomAliasHelper, ) { private val createRoomConfigFlow: MutableStateFlow = MutableStateFlow(CreateRoomConfig()) diff --git a/features/createroom/impl/src/main/res/values-da/translations.xml b/features/createroom/impl/src/main/res/values-da/translations.xml index a8f66f60db..1a69f150fb 100644 --- a/features/createroom/impl/src/main/res/values-da/translations.xml +++ b/features/createroom/impl/src/main/res/values-da/translations.xml @@ -1,7 +1,7 @@ "Nyt rum" - "Invitér folk" + "Invitér andre" "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" 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 213d10ae7a..d7df703abb 100644 --- a/features/createroom/impl/src/main/res/values-de/translations.xml +++ b/features/createroom/impl/src/main/res/values-de/translations.xml @@ -1,22 +1,22 @@ - "Neuer Raum" + "Neuer Chat" "Nutzer einladen" "Beim Erstellen des Chats ist ein Fehler aufgetreten" - "Nur eingeladene Personen haben Zutritt zu diesem Chatroom. Alle Nachrichten sind Ende-zu-Ende verschlüsselt." - "Privater Chatroom" - "Alle können diesen Chatroom finden. -Sie können dies aber jederzeit in den Chatroomeinstellungen ändern." - "Öffentlicher Raum" - "Jeder darf diesen Raum betreten" + "Nur eingeladene Personen haben Zutritt zu diesem Chat. Alle Nachrichten sind Ende-zu-Ende verschlüsselt." + "Privater Chat" + "Jeder kann diesen Chat finden. +Du kannst dies jederzeit in den Einstellungen des Chats ändern." + "Öffentlicher Chat" + "Jeder darf diesem Chat beitreten" "Jeder" - "Chatroomzugang" - "Jeder kann den Zutritt zum Raum beantragen, aber ein Moderator muss die Anfrage akzeptieren." + "Chat Zugang" + "Jeder kann den Beitritt zum Chat erbitten, aber ein Admin oder Moderator muss die Anfrage akzeptieren." "Beitritt beantragen" - "Damit dieser Chatroom im öffentlichen Chatroomverzeichnis sichtbar ist, benötigen Sie eine Chatroomadresse." - "Chatroomadresse" - "Raumname" - " Sichtbarkeit des Chatrooms" - "Raum erstellen" + "Du benötigst eine Chat-Adresse, damit dieser Chat im öffentlichen Verzeichnis sichtbar ist." + "Chat-Adresse" + "Chat-Name" + " Sichtbarkeit des Chats" + "Chat erstellen" "Thema (optional)" diff --git a/features/createroom/impl/src/main/res/values-ko/translations.xml b/features/createroom/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..cac10c0b1f --- /dev/null +++ b/features/createroom/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,22 @@ + + + "새 방" + "사람 초대하기" + "방을 생성하던 중 오류가 발생했어요" + "초대받은 사람만 이 방에 액세스할 수 있습니다. 모든 메시지는 종단 간 암호화됩니다." + "비공개 방" + "누구나 이 방을 찾을 수 있습니다. +방 설정에서 언제든지 변경할 수 있습니다." + "공개 방" + "누구나 이 방에 참여할 수 있습니다." + "누구나" + "방 액세스" + "누구나 방에 참여 요청을 할 수 있지만, 관리자나 운영자가 요청을 수락해야 합니다." + "참가 요청" + "이 방이 공개 방 디렉토리에 표시되려면 방 주소가 필요합니다." + "방 주소" + "방 이름" + "방 표시 여부" + "방 만들기" + "주제 (선택)" + 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 1b1e9f2d1b..399c9fec17 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 @@ -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 criptografadas de ponta a ponta." + "Apenas as pessoas convidadas podem entrar nesta sala. Todas as mensagens são criptografadas de ponta a ponta." "Sala privada" "Qualquer um pode encontrar esta sala. Você pode mudar isso a qualquer momento nas configurações da sala." 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 a5ea4fbb85..b9fe78bb19 100644 --- a/features/createroom/impl/src/main/res/values-ro/translations.xml +++ b/features/createroom/impl/src/main/res/values-ro/translations.xml @@ -11,10 +11,12 @@ Puteți modifica acest lucru oricând în setări." "Oricine se poate alătura acestei camere" "Oricine" "Acces la cameră" - "Oricine poate cere să se alăture camerei, dar un administrator sau un moderator va trebui să accepte solicitarea" + "Oricine poate cere să se alăture camerei, dar un administrator sau un moderator va trebui să accepte cererea" "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" + "Vizibilitatea camerei" "Creați o cameră" "Subiect (opțional)" 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 c2591ff235..34062f9669 100644 --- a/features/createroom/impl/src/main/res/values-uz/translations.xml +++ b/features/createroom/impl/src/main/res/values-uz/translations.xml @@ -7,7 +7,15 @@ "Shaxsiy xona" "Bu xonani har kim topishi mumkin. Buni xona sozlamalaridan istalgan vaqtda oʻzgartirishingiz mumkin." + "Jamoat xonasi" + "Bu xonaga istalgan kishi qo‘shilishi mumkin" + "Har kim" + "Xonaga kirish" + "Xonaga qo‘shilishni istalgan kishi so‘rashi mumkin, lekin administrator yoki moderator so‘rovni qabul qilishi kerak" + "Qo‘shilishni so‘rang" + "Ushbu xona ommaviy xonalar ro‘yxatida ko‘rinishi uchun sizga xona manzili kerak bo‘ladi." "Xona nomi" + "Xonaning ko‘rinishi" "Xonani yaratish" "Mavzu (ixtiyoriy)" diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPointTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPointTest.kt new file mode 100644 index 0000000000..61c7c052c5 --- /dev/null +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPointTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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 androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.createroom.api.CreateRoomEntryPoint +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultCreateRoomEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultCreateRoomEntryPoint() + + val parentNode = TestParentNode.create { buildContext, plugins -> + CreateRoomFlowNode( + buildContext = buildContext, + plugins = plugins, + ) + } + val callback = object : CreateRoomEntryPoint.Callback { + override fun onRoomCreated(roomId: RoomId) = lambdaError() + } + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .callback(callback) + .build() + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/deactivation/impl/build.gradle.kts b/features/deactivation/impl/build.gradle.kts index 7694ac8bc1..842206bab7 100644 --- a/features/deactivation/impl/build.gradle.kts +++ b/features/deactivation/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2024 New Vector Ltd. @@ -22,7 +23,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.androidutils) @@ -34,14 +35,6 @@ dependencies { implementation(projects.libraries.uiStrings) api(projects.features.deactivation.api) - 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) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.tests.testutils) } diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationNode.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationNode.kt index 59c45c5bb5..3e554672a8 100644 --- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationNode.kt +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationNode.kt @@ -12,13 +12,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class AccountDeactivationNode @AssistedInject constructor( +@AssistedInject +class AccountDeactivationNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: AccountDeactivationPresenter, diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt index d8e8137d2d..eb751366a8 100644 --- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt @@ -12,15 +12,16 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.MatrixClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import javax.inject.Inject -class AccountDeactivationPresenter @Inject constructor( +@Inject +class AccountDeactivationPresenter( private val matrixClient: MatrixClient, ) : Presenter { @Composable diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPoint.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPoint.kt index 5481db19d7..0f34e18b9f 100644 --- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPoint.kt +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPoint.kt @@ -9,14 +9,15 @@ package io.element.android.features.logout.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.deactivation.api.AccountDeactivationEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultAccountDeactivationEntryPoint @Inject constructor() : AccountDeactivationEntryPoint { +@Inject +class DefaultAccountDeactivationEntryPoint : AccountDeactivationEntryPoint { override fun createNode(parentNode: Node, buildContext: BuildContext): Node { return parentNode.createNode(buildContext) } diff --git a/features/deactivation/impl/src/main/res/values-da/translations.xml b/features/deactivation/impl/src/main/res/values-da/translations.xml index e9d5d9fdda..c6dcb1710a 100644 --- a/features/deactivation/impl/src/main/res/values-da/translations.xml +++ b/features/deactivation/impl/src/main/res/values-da/translations.xml @@ -6,8 +6,8 @@ "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" + "Permanent deaktivere" + "Fjerne dig fra alle samtaler" "Slette dine kontooplysninger fra vores identitetsserver." "Dine beskeder vil stadig være synlige for registrerede brugere, men vil ikke være tilgængelige for nye eller uregistrerede brugere, hvis du vælger at slette dem." "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 cd61a6a9d0..1aec7495a1 100644 --- a/features/deactivation/impl/src/main/res/values-de/translations.xml +++ b/features/deactivation/impl/src/main/res/values-de/translations.xml @@ -1,14 +1,14 @@ - "Bitte bestätigen Sie, dass Sie Ihr Benutzerkonto deaktivieren möchten. Diese Aktion kann nicht rückgängig gemacht werden." + "Bitte bestätige, dass du dein Konto deaktivieren möchtest. Dies kann nicht rückgängig gemacht werden." "Lösche alle meine Nachrichten" - "Warnung: Benutzern werden möglicherweise unvollständige Konversationen angezeigt." - "Wenn Sie Ihr Konto deaktivieren%1$s, wird es:" + "Warnung: Künftigen Nutzern werden möglicherweise unvollständige Konversationen angezeigt." + "Dein Konto zu deaktivieren ist %1$s. Folgendes wird passieren:" "irreversibel" - "%1$s Ihr Konto (Sie können sich nicht erneut anmelden und Ihre ID kann nicht wiederverwendet werden)." + "%1$s dein Konto (du kannst dich nicht erneut anmelden und deine ID kann nicht wiederverwendet werden)." "Dauerhaft deaktivieren" - "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." + "Du wirst aus allen Chats entfernt." + "Lösche deine Kontoinformationen von unserem Identitätsserver." + "Deine Nachrichten werden für bereits registrierte Nutzer weiterhin sichtbar sein. Für neue oder unregistrierte Nutzer sind sie nicht verfügbar, wenn du sie löschen solltest." "Nutzerkonto deaktivieren" diff --git a/features/deactivation/impl/src/main/res/values-eo/translations.xml b/features/deactivation/impl/src/main/res/values-eo/translations.xml new file mode 100644 index 0000000000..75c2c252e7 --- /dev/null +++ b/features/deactivation/impl/src/main/res/values-eo/translations.xml @@ -0,0 +1,4 @@ + + + "Delete your account information from our server." + diff --git a/features/deactivation/impl/src/main/res/values-eu/translations.xml b/features/deactivation/impl/src/main/res/values-eu/translations.xml index d55c272e65..3df431cd8c 100644 --- a/features/deactivation/impl/src/main/res/values-eu/translations.xml +++ b/features/deactivation/impl/src/main/res/values-eu/translations.xml @@ -1,9 +1,12 @@ + "Baieztatu zure kontua desaktibatu nahi duzula. Ekintza hau ezin da desegin." "Ezabatu nire mezu guztiak" "Kontuaren desaktibazioa %1$s, honakoa eragingo du:" "ezin da desegin" "Ezgaitu betiko" "Kendu zure burua txat gela guztietatik." + "Ezabatu zure kontuaren informazioa gure identitate-zerbitzaritik." + "Zure mezuak erregistratutako erabiltzaileentzat ikusgai egongo dira oraindik, baina ezabatzen badituzu, ez dira eskuragarri egongo erabiltzaile berri edo erregistratu gabeentzat." "Desaktibatu kontua" diff --git a/features/deactivation/impl/src/main/res/values-hu/translations.xml b/features/deactivation/impl/src/main/res/values-hu/translations.xml index 47651f0ff9..3d3722b8ef 100644 --- a/features/deactivation/impl/src/main/res/values-hu/translations.xml +++ b/features/deactivation/impl/src/main/res/values-hu/translations.xml @@ -8,7 +8,7 @@ "%1$s a fiókját (nem fog tudni újra bejelentkezni, és az azonosítója nem használható újra)." "Véglegesen letiltja" "Eltávolításra kerül az összes csevegőszobából." - "Törlésre kerülnek a fiókadatai a személyazonosító kiszolgálónkról." + "Törlésre kerülnek a fiókadatai az azonosítási kiszolgálónkról." "Üzenetei továbbra is láthatóak maradnak a regisztrált felhasználók számára, de nem lesznek elérhetőek az új vagy nem regisztrált felhasználók számára, ha úgy dönt, hogy törli őket." "Fiók deaktiválása" diff --git a/features/deactivation/impl/src/main/res/values-ko/translations.xml b/features/deactivation/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..6b7953a4a5 --- /dev/null +++ b/features/deactivation/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,14 @@ + + + "계정을 비활성화하시겠습니까? 이 작업은 되돌릴 수 없습니다." + "모든 내 메시지 삭제" + "경고: 향후 사용자는 불완전한 대화 내용을 볼 수 있습니다." + "계정을 비활성화하는 것은 %1$s 이며, 다음과 같은 조치를 취합니다:" + "불가역적" + "%1$s 귀하의 계정 (로그인할 수 없으며, 귀하의 ID는 재사용할 수 없습니다)." + "영구적으로 비활성화" + "모든 채팅방에서 자신을 제거하세요." + "당사의 신원 서버에서 귀하의 계정 정보를 삭제하세요." + "메시지는 등록된 사용자에게는 계속 표시되지만, 삭제하면 신규 또는 미등록 사용자는 볼 수 없게 됩니다." + "계정 비활성화" + 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 51a412a7a3..7000a65d47 100644 --- a/features/deactivation/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/deactivation/impl/src/main/res/values-pt-rBR/translations.xml @@ -1,14 +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." + "Apagar todas as minhas mensagens" + "Alerta: Usuários futuros podem 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" + "%1$s (você não poderá entrar novamente, e seu ID não poderá ser reutilizado)." + "Desativar a sua conta 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." + "Apague as informações da sua conta do nosso servidor de identidade." + "Suas mensagens ainda estarão visíveis para os usuários registrados, mas não estarão disponíveis para usuários novos ou não registrados se você optar por apagá-las." "Desativar conta" diff --git a/features/deactivation/impl/src/main/res/values-pt/translations.xml b/features/deactivation/impl/src/main/res/values-pt/translations.xml index 536f4cec34..0a8c618e44 100644 --- a/features/deactivation/impl/src/main/res/values-pt/translations.xml +++ b/features/deactivation/impl/src/main/res/values-pt/translations.xml @@ -1,6 +1,6 @@ - "Confirme que pretende desativar a sua conta. Esta ação não pode ser desfeita." + "Confirma que pretendes desativar a tua conta. Esta ação não pode ser desfeita." "Eliminar todas as minhas mensagens" "Aviso: futuros usuários podem ver conversas incompletas." "A desativação da sua conta é %1$s, irá:" diff --git a/features/deactivation/impl/src/main/res/values-ro/translations.xml b/features/deactivation/impl/src/main/res/values-ro/translations.xml index 2fe403ec23..acd4c0747d 100644 --- a/features/deactivation/impl/src/main/res/values-ro/translations.xml +++ b/features/deactivation/impl/src/main/res/values-ro/translations.xml @@ -7,7 +7,7 @@ "ireversibilă" "%1$s contul dumneavoastră (nu vă puteți conecta din nou, iar ID-ul dvs. nu poate fi reutilizat)." "Dezactivați permanent" - "Elimina din toate camerele de chat." + "Îndepărta din toate camerele de chat." "Șterge informațiile contului dumneavoastră de pe serverul nostru de identitate." "Mesajele dumneavoastră vor fi în continuare vizibile pentru utilizatorii înregistrați, dar nu vor fi disponibile pentru utilizatorii noi sau neînregistrați dacă alegeți să le ștergeți." "Dezactivați contul" diff --git a/features/deactivation/impl/src/main/res/values-uz/translations.xml b/features/deactivation/impl/src/main/res/values-uz/translations.xml index 07a873d2e3..19a70bb149 100644 --- a/features/deactivation/impl/src/main/res/values-uz/translations.xml +++ b/features/deactivation/impl/src/main/res/values-uz/translations.xml @@ -1,4 +1,14 @@ + "Iltimos, hisobingizni o‘chirishni xohlayotganingizni tasdiqlang. Bu amalni qaytarib bo‘lmaydi." + "Barcha xabarlarimni o‘chirib tashlang" + "Ogohlantirish: Kelgusi foydalanuvchilar chala suhbatlarni ko‘rishi mumkin." + "Hisobingiz %1$s faolsizlantirilmoqda, u quyidagilarni bajaradi:" + "qaytarilmas" + "%1$s hisobingiz (qaytadan kirolmaysiz va ID qayta ishlatilmaydi)." + "Butunlay faolsizlantirish" + "Sizni barcha chat xonalaridan olib tashlash." + "Hisobingiz haqidagi axborotni identifikatsiya serverimizdan o‘chirib tashlang." + "Xabarlaringiz ro‘yxatdan o‘tgan foydalanuvchilarga ko‘rinadi, lekin ularni o‘chirishni tanlasangiz, yangi yoki ro‘yxatdan o‘tmagan foydalanuvchilarga ko‘rinmaydi." "Hisobni faolsizlantirish" diff --git a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt index 1383bdde21..d0b9f57dd9 100644 --- a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt +++ b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt @@ -148,10 +148,10 @@ class AccountDeactivationPresenterTest { assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Uninitialized) } } - - private fun createPresenter( - matrixClient: MatrixClient = FakeMatrixClient(), - ) = AccountDeactivationPresenter( - matrixClient = matrixClient, - ) } + +internal fun createPresenter( + matrixClient: MatrixClient = FakeMatrixClient(), +) = AccountDeactivationPresenter( + matrixClient = matrixClient, +) diff --git a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPointTest.kt b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPointTest.kt new file mode 100644 index 0000000000..05ad52efe9 --- /dev/null +++ b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPointTest.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.features.logout.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultAccountDeactivationEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultAccountDeactivationEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + AccountDeactivationNode( + buildContext = buildContext, + plugins = plugins, + presenter = createPresenter(), + ) + } + val result = entryPoint.createNode(parentNode, BuildContext.root(null)) + assertThat(result).isInstanceOf(AccountDeactivationNode::class.java) + } +} diff --git a/features/enterprise/impl/build.gradle.kts b/features/enterprise/impl-foss/build.gradle.kts similarity index 72% rename from features/enterprise/impl/build.gradle.kts rename to features/enterprise/impl-foss/build.gradle.kts index 642b2fef40..956c0e1900 100644 --- a/features/enterprise/impl/build.gradle.kts +++ b/features/enterprise/impl-foss/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2024 New Vector Ltd. @@ -14,17 +15,14 @@ android { namespace = "io.element.android.features.enterprise.impl" } -setupAnvil() +setupDependencyInjection() dependencies { implementation(libs.compound) - implementation(projects.anvilannotations) api(projects.features.enterprise.api) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) } diff --git a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt similarity index 86% rename from features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt rename to features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt index d5c23788d6..4d52e83a8f 100644 --- a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt +++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt @@ -7,19 +7,20 @@ package io.element.android.features.enterprise.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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.BugReportUrl import io.element.android.features.enterprise.api.EnterpriseService -import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.coroutines.flow.flowOf -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultEnterpriseService @Inject constructor() : EnterpriseService { +@Inject +class DefaultEnterpriseService : EnterpriseService { override val isEnterpriseBuild = false override suspend fun isEnterpriseUser(sessionId: SessionId) = false diff --git a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt similarity index 74% rename from features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt rename to features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt index 47f52f6ac1..f25c38531a 100644 --- a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt +++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt @@ -7,13 +7,14 @@ package io.element.android.features.enterprise.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 { +@Inject +class DefaultSessionEnterpriseService : SessionEnterpriseService { override suspend fun init() = Unit 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-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt similarity index 100% rename from features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt rename to features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt diff --git a/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseServiceTest.kt b/features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseServiceTest.kt similarity index 100% rename from features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseServiceTest.kt rename to features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseServiceTest.kt diff --git a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/FtueEntryPoint.kt b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/FtueEntryPoint.kt index f719064fde..3d5aae3860 100644 --- a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/FtueEntryPoint.kt +++ b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/FtueEntryPoint.kt @@ -7,14 +7,6 @@ package io.element.android.features.ftue.api -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.architecture.SimpleFeatureEntryPoint -interface FtueEntryPoint : FeatureEntryPoint { - fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder - - interface NodeBuilder { - fun build(): Node - } -} +interface FtueEntryPoint : SimpleFeatureEntryPoint diff --git a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueService.kt b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueService.kt index 7ea26c548f..b596f328d5 100644 --- a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueService.kt +++ b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueService.kt @@ -15,9 +15,6 @@ import kotlinx.coroutines.flow.StateFlow interface FtueService { /** The current state of the FTUE. */ val state: StateFlow - - /** Reset the FTUE state. */ - suspend fun reset() } /** The state of the FTUE. */ diff --git a/features/ftue/impl/build.gradle.kts b/features/ftue/impl/build.gradle.kts index e95a4697ae..d7d61f6d8d 100644 --- a/features/ftue/impl/build.gradle.kts +++ b/features/ftue/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -22,7 +23,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.ftue.api) @@ -33,6 +34,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.uiCommon) implementation(projects.libraries.uiStrings) implementation(projects.libraries.testtags) implementation(projects.features.analytics.api) @@ -46,14 +48,7 @@ dependencies { 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) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.noop) @@ -61,5 +56,4 @@ dependencies { testImplementation(projects.libraries.preferences.test) testImplementation(projects.features.lockscreen.test) testImplementation(projects.services.toolbox.test) - testImplementation(projects.tests.testutils) } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPoint.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPoint.kt index ee0a3d87ef..4fa086f4cd 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPoint.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPoint.kt @@ -9,22 +9,16 @@ package io.element.android.features.ftue.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultFtueEntryPoint @Inject constructor() : FtueEntryPoint { - override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): FtueEntryPoint.NodeBuilder { - val plugins = ArrayList() - - return object : FtueEntryPoint.NodeBuilder { - override fun build(): Node { - return parentNode.createNode(buildContext, plugins) - } - } +@Inject +class DefaultFtueEntryPoint : FtueEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext): Node { + return parentNode.createNode(buildContext) } } 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 71a27dcaae..6552a3b360 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 @@ -8,49 +8,42 @@ package io.element.android.features.ftue.impl import android.os.Parcelable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope -import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.newRoot import com.bumble.appyx.navmodel.backstack.operation.replace -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.analytics.api.AnalyticsEntryPoint import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode import io.element.android.features.ftue.impl.sessionverification.FtueSessionVerificationFlowNode import io.element.android.features.ftue.impl.state.DefaultFtueService import io.element.android.features.ftue.impl.state.FtueStep +import io.element.android.features.ftue.impl.state.InternalFtueState import io.element.android.features.lockscreen.api.LockScreenEntryPoint 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.theme.components.CircularProgressIndicator -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.distinctUntilChanged -import kotlinx.coroutines.flow.filter +import io.element.android.libraries.ui.common.nodes.emptyNode +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -class FtueFlowNode @AssistedInject constructor( +@AssistedInject +class FtueFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val ftueState: DefaultFtueService, + private val defaultFtueService: DefaultFtueService, private val analyticsEntryPoint: AnalyticsEntryPoint, - private val analyticsService: AnalyticsService, private val lockScreenEntryPoint: LockScreenEntryPoint, ) : BaseFlowNode( backstack = BackStack( @@ -79,31 +72,23 @@ class FtueFlowNode @AssistedInject constructor( override fun onBuilt() { super.onBuilt() - - lifecycle.subscribe(onCreate = { - moveToNextStepIfNeeded() - }) - - analyticsService.didAskUserConsentFlow - .distinctUntilChanged() - .onEach { moveToNextStepIfNeeded() } - .launchIn(lifecycleScope) - - ftueState.isVerificationStatusKnown - .filter { it } - .onEach { moveToNextStepIfNeeded() } + defaultFtueService.ftueStepStateFlow + .filterIsInstance(InternalFtueState.Incomplete::class) + .onEach { + showStep(it.nextStep) + } .launchIn(lifecycleScope) } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Placeholder -> { - createNode(buildContext) + emptyNode(buildContext) } is NavTarget.SessionVerification -> { val callback = object : FtueSessionVerificationFlowNode.Callback { override fun onDone() { - moveToNextStepIfNeeded() + defaultFtueService.onUserCompletedSessionVerification() } } createNode(buildContext, listOf(callback)) @@ -111,7 +96,7 @@ class FtueFlowNode @AssistedInject constructor( NavTarget.NotificationsOptIn -> { val callback = object : NotificationsOptInNode.Callback { override fun onNotificationsOptInFinished() { - moveToNextStepIfNeeded() + defaultFtueService.updateFtueStep() } } createNode(buildContext, listOf(callback)) @@ -122,7 +107,7 @@ class FtueFlowNode @AssistedInject constructor( NavTarget.LockScreenSetup -> { val callback = object : LockScreenEntryPoint.Callback { override fun onSetupDone() { - moveToNextStepIfNeeded() + defaultFtueService.updateFtueStep() } } lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Setup) @@ -132,8 +117,8 @@ class FtueFlowNode @AssistedInject constructor( } } - private fun moveToNextStepIfNeeded() = lifecycleScope.launch { - when (ftueState.getNextStep()) { + private fun showStep(ftueStep: FtueStep) { + when (ftueStep) { FtueStep.WaitingForInitialState -> { backstack.newRoot(NavTarget.Placeholder) } @@ -149,7 +134,6 @@ class FtueFlowNode @AssistedInject constructor( FtueStep.LockscreenSetup -> { backstack.newRoot(NavTarget.LockScreenSetup) } - null -> Unit } } @@ -157,17 +141,4 @@ class FtueFlowNode @AssistedInject constructor( override fun View(modifier: Modifier) { BackstackView() } - - @ContributesNode(AppScope::class) - class PlaceholderNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - ) : Node(buildContext, plugins = plugins) { - @Composable - override fun View(modifier: Modifier) { - Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } - } - } } 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 index 4387b3dc00..656ffac512 100644 --- 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 @@ -7,16 +7,16 @@ package io.element.android.features.ftue.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo 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 +@BindingContainer interface FtueModule { @Binds fun bindChooseSelfVerificationMethodPresenter(presenter: ChooseSelfVerificationModePresenter): Presenter diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInNode.kt index 6df334b6df..6e13e23a31 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInNode.kt @@ -12,15 +12,16 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class NotificationsOptInNode @AssistedInject constructor( +@AssistedInject +class NotificationsOptInNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: NotificationsOptInPresenter.Factory, 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 5db0955c5a..b6f5d76351 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 @@ -12,9 +12,9 @@ import android.os.Build import androidx.annotation.RequiresApi import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.permissions.api.PermissionStateProvider @@ -25,7 +25,8 @@ import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class NotificationsOptInPresenter @AssistedInject constructor( +@AssistedInject +class NotificationsOptInPresenter( permissionsPresenterFactory: PermissionsPresenter.Factory, @Assisted private val callback: NotificationsOptInNode.Callback, @AppCoroutineScope 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 a37fdd2192..02a27d381d 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 @@ -20,9 +20,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.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 @@ -37,7 +37,8 @@ import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -class FtueSessionVerificationFlowNode @AssistedInject constructor( +@AssistedInject +class FtueSessionVerificationFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint, 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 index 687dc4aefe..99409ac2d2 100644 --- 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 @@ -13,15 +13,16 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.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( +@AssistedInject +class ChooseSelfVerificationModeNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: Presenter, 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 index 0aec3b4e1d..e278e803c4 100644 --- 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 @@ -12,14 +12,15 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import dev.zacsweers.metro.Inject 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( +@Inject +class ChooseSelfVerificationModePresenter( private val encryptionService: EncryptionService, private val directLogoutPresenter: Presenter, ) : Presenter { 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 744053b976..40f19b1e7a 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 @@ -9,13 +9,14 @@ package io.element.android.features.ftue.impl.state import android.Manifest import android.os.Build -import androidx.annotation.VisibleForTesting -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.lockscreen.api.LockScreenService +import io.element.android.libraries.core.coroutine.mapState 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.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus @@ -25,61 +26,70 @@ import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import javax.inject.Inject +import kotlinx.coroutines.launch @ContributesBinding(SessionScope::class) @SingleIn(SessionScope::class) -class DefaultFtueService @Inject constructor( +@Inject +class DefaultFtueService( private val sdkVersionProvider: BuildVersionSdkIntProvider, - @SessionCoroutineScope sessionCoroutineScope: CoroutineScope, + @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val analyticsService: AnalyticsService, private val permissionStateProvider: PermissionStateProvider, private val lockScreenService: LockScreenService, private val sessionVerificationService: SessionVerificationService, private val sessionPreferencesStore: SessionPreferencesStore, ) : FtueService { - override val state = MutableStateFlow(FtueState.Unknown) + private val userNeedsToConfirmSessionVerificationSuccess = MutableStateFlow(false) - /** - * This flow emits true when the FTUE flow is ready to be displayed. - * In this case, the FTUE flow is ready when the session verification status is known. - */ - val isVerificationStatusKnown = sessionVerificationService.sessionVerifiedStatus - .map { it != SessionVerifiedStatus.Unknown } - .distinctUntilChanged() + val ftueStepStateFlow = MutableStateFlow(InternalFtueState.Unknown) - override suspend fun reset() { - analyticsService.reset() - if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) { - permissionStateProvider.resetPermission(Manifest.permission.POST_NOTIFICATIONS) + override val state = ftueStepStateFlow + .mapState { + when (it) { + is InternalFtueState.Unknown -> FtueState.Unknown + is InternalFtueState.Incomplete -> FtueState.Incomplete + is InternalFtueState.Complete -> FtueState.Complete + } + } + + init { + combine( + sessionVerificationService.sessionVerifiedStatus.onEach { sessionVerifiedStatus -> + if (sessionVerifiedStatus == SessionVerifiedStatus.NotVerified) { + // Ensure we wait for the user to confirm the session verified screen before going further + userNeedsToConfirmSessionVerificationSuccess.value = true + } + }, + userNeedsToConfirmSessionVerificationSuccess, + analyticsService.didAskUserConsentFlow.distinctUntilChanged(), + ) { + updateFtueStep() + } + .launchIn(sessionCoroutineScope) + } + + fun updateFtueStep() = sessionCoroutineScope.launch { + val step = getNextStep(null) + ftueStepStateFlow.value = when (step) { + null -> InternalFtueState.Complete + else -> InternalFtueState.Incomplete(step) } } - init { - sessionVerificationService.sessionVerifiedStatus - .onEach { updateState() } - .launchIn(sessionCoroutineScope) - - analyticsService.didAskUserConsentFlow - .distinctUntilChanged() - .onEach { updateState() } - .launchIn(sessionCoroutineScope) - } - - suspend fun getNextStep(currentStep: FtueStep? = null): FtueStep? = - when (currentStep) { + private suspend fun getNextStep(completedStep: FtueStep? = null): FtueStep? = + when (completedStep) { null -> if (!isSessionVerificationStateReady()) { FtueStep.WaitingForInitialState } else { getNextStep(FtueStep.WaitingForInitialState) } - FtueStep.WaitingForInitialState -> if (isSessionNotVerified()) { + FtueStep.WaitingForInitialState -> if (isSessionNotVerified() || userNeedsToConfirmSessionVerificationSuccess.value) { FtueStep.SessionVerification } else { getNextStep(FtueStep.SessionVerification) @@ -107,9 +117,6 @@ class DefaultFtueService @Inject constructor( } private suspend fun isSessionNotVerified(): Boolean { - // Wait until the session verification status is known - isVerificationStatusKnown.filter { it }.first() - return sessionVerificationService.sessionVerifiedStatus.value == SessionVerifiedStatus.NotVerified && !canSkipVerification() } @@ -136,14 +143,8 @@ class DefaultFtueService @Inject constructor( return lockScreenService.isSetupRequired().first() } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal suspend fun updateState() { - val nextStep = getNextStep() - state.value = when { - // Final state, there aren't any more next steps - nextStep == null -> FtueState.Complete - else -> FtueState.Incomplete - } + fun onUserCompletedSessionVerification() { + userNeedsToConfirmSessionVerificationSuccess.value = false } } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/InternalFtueState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/InternalFtueState.kt new file mode 100644 index 0000000000..c620a2ca5b --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/InternalFtueState.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.ftue.impl.state + +sealed interface InternalFtueState { + data object Unknown : InternalFtueState + + data class Incomplete( + val nextStep: FtueStep, + ) : InternalFtueState + + data object Complete : InternalFtueState +} 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 773cdc09f8..6d902ab8d4 100644 --- a/features/ftue/impl/src/main/res/values-de/translations.xml +++ b/features/ftue/impl/src/main/res/values-de/translations.xml @@ -2,15 +2,15 @@ "Bestätigung unmöglich?" "Erstelle einen neuen Wiederherstellungsschlüssel" - "Verifiziere dieses Gerät, um sicheres Messaging einzurichten." - "Bestätigen Sie Ihre Identität" + "Verifiziere dieses Gerät, um sichere Chats einzurichten." + "Bestätige deine 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." + "Du kannst jetzt verschlüsselte Nachrichten lesen und versenden. Dein Chatpartner vertraut nun diesem Gerät." "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." + "Du kannst deine Einstellungen später ändern." "Erlaube Benachrichtigungen und verpasse keine Nachricht" "Wiederherstellungsschlüssel eingeben" diff --git a/features/ftue/impl/src/main/res/values-eo/translations.xml b/features/ftue/impl/src/main/res/values-eo/translations.xml new file mode 100644 index 0000000000..fae7da561c --- /dev/null +++ b/features/ftue/impl/src/main/res/values-eo/translations.xml @@ -0,0 +1,9 @@ + + + "Create a new backup password" + "Confirm this device to set up secure messaging." + "Confirm it\'s you" + "Use backup password" + "Device confirmed" + "Enter backup password" + diff --git a/features/ftue/impl/src/main/res/values-ko/translations.xml b/features/ftue/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..cb7c9e32dc --- /dev/null +++ b/features/ftue/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,16 @@ + + + "확인할 수 없나요?" + "새로운 복구 키 만들기" + "보안 메시징을 설정하려면 이 장치를 확인하세요." + "본인 확인" + "다른 기기 사용" + "복구 키 사용" + "이제 메시지를 안전하게 읽거나 보낼 수 있으며, 채팅 상대도 이 기기를 신뢰할 수 있습니다." + "기기 검증됨" + "다른 기기 사용" + "다른 기기에서 대기 중…" + "나중에 설정을 변경할 수 있습니다." + "알림을 허용하고 메시지를 놓치지 마세요." + "복구 키를 입력하세요" + 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 6e280bf479..8629bbfd11 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 @@ -2,15 +2,15 @@ "Não consegue confirmar?" "Criar uma nova chave de recuperação" - "Verifique este dispositivo para configurar mensagens seguras." + "Verifique este dispositivo para configurar as mensagens seguras." "Confirme sua identidade" "Usar outro dispositivo" - "Use a chave de recuperação" + "Usar 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…" + "Aguardando o outro dispositivo…" "Você pode alterar suas configurações mais tarde." - "Permita notificações e nunca perca uma mensagem" - "Insira a chave de recuperação" + "Permita as notificações e nunca perca uma mensagem" + "Digitar chave de recuperação" diff --git a/features/ftue/impl/src/main/res/values-uz/translations.xml b/features/ftue/impl/src/main/res/values-uz/translations.xml index c283423486..9ed2a2b86d 100644 --- a/features/ftue/impl/src/main/res/values-uz/translations.xml +++ b/features/ftue/impl/src/main/res/values-uz/translations.xml @@ -1,5 +1,16 @@ + "Tasdiqlay olmayapsizmi?" + "Yangi tiklash kalitini yarating" + "Xavfsiz xabarlashuvni sozlash uchun ushbu qurilmani tasdiqlang." + "Shaxsingizni tasdiqlang" + "Boshqa qurilmadan foydalanish" + "Qayta tiklash kalitidan foydalaning" + "Endi xabarlarni xavfsiz tarzda o‘qish yoki yuborish imkoniyatiga egasiz, shuningdek, siz bilan muloqot qilayotgan har qanday kishi ham bu qurilmaga ishonch bildirishi mumkin." + "Qurilma tasdiqlandi" + "Boshqa qurilmadan foydalanish" + "Boshqa qurilmada kutilmoqda…" "Sozlamalaringizni keyinroq o\'zgartirishingiz mumkin." "Bildirishnomalarga ruxsat bering va hech qachon xabarni o\'tkazib yubormang" + "Tiklash kalitini kiriting" diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPointTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPointTest.kt new file mode 100644 index 0000000000..3a8ed11ea2 --- /dev/null +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPointTest.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.ftue.impl + +import android.content.Context +import android.content.Intent +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.api.LockScreenEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class DefaultFtueEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() = runTest { + val entryPoint = DefaultFtueEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + FtueFlowNode( + buildContext = buildContext, + plugins = plugins, + analyticsEntryPoint = { _, _ -> lambdaError() }, + defaultFtueService = createDefaultFtueService(), + lockScreenEntryPoint = object : LockScreenEntryPoint { + override fun nodeBuilder( + parentNode: com.bumble.appyx.core.node.Node, + buildContext: BuildContext, + navTarget: LockScreenEntryPoint.Target + ): LockScreenEntryPoint.NodeBuilder { + lambdaError() + } + + override fun pinUnlockIntent(context: Context): Intent { + lambdaError() + } + }, + ) + } + val result = entryPoint.createNode(parentNode, BuildContext.root(null)) + assertThat(result).isInstanceOf(FtueFlowNode::class.java) + } +} 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 4a05433e5e..e46f43f3c3 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 @@ -13,6 +13,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.ftue.impl.state.DefaultFtueService import io.element.android.features.ftue.impl.state.FtueStep +import io.element.android.features.ftue.impl.state.InternalFtueState import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.test.FakeLockScreenService import io.element.android.libraries.matrix.api.verification.SessionVerificationService @@ -26,8 +27,6 @@ import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.noop.NoopAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider -import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -69,9 +68,11 @@ class DefaultFtueServiceTest { analyticsService.setDidAskUserConsent() permissionStateProvider.setPermissionGranted() lockScreenService.setIsPinSetup(true) - service.updateState() - - assertThat(service.state.value).isEqualTo(FtueState.Complete) + service.updateFtueStep() + service.state.test { + assertThat(awaitItem()).isEqualTo(FtueState.Unknown) + assertThat(awaitItem()).isEqualTo(FtueState.Complete) + } } @Test @@ -90,9 +91,11 @@ class DefaultFtueServiceTest { sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified) permissionStateProvider.setPermissionGranted() lockScreenService.setIsPinSetup(true) - service.updateState() - - assertThat(service.state.value).isEqualTo(FtueState.Complete) + service.updateFtueStep() + service.state.test { + assertThat(awaitItem()).isEqualTo(FtueState.Unknown) + assertThat(awaitItem()).isEqualTo(FtueState.Complete) + } } @Test @@ -109,35 +112,30 @@ class DefaultFtueServiceTest { permissionStateProvider = permissionStateProvider, lockScreenService = lockScreenService, ) - val steps = mutableListOf() - // Session verification - steps.add(service.getNextStep(steps.lastOrNull())) - sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.NotVerified) - - // Notifications opt in - steps.add(service.getNextStep(steps.lastOrNull())) - permissionStateProvider.setPermissionGranted() - - // Entering PIN code - steps.add(service.getNextStep(steps.lastOrNull())) - lockScreenService.setIsPinSetup(true) - - // Analytics opt in - steps.add(service.getNextStep(steps.lastOrNull())) - analyticsService.setDidAskUserConsent() - - // Final step (null) - steps.add(service.getNextStep(steps.lastOrNull())) - - assertThat(steps).containsExactly( - FtueStep.SessionVerification, - FtueStep.NotificationsOptIn, - FtueStep.LockscreenSetup, - FtueStep.AnalyticsOptIn, - // Final state - null, - ) + service.ftueStepStateFlow.test { + assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown) + // Session verification + assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.SessionVerification)) + sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified) + // User completes verification + service.onUserCompletedSessionVerification() + // Notifications opt in + assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.NotificationsOptIn)) + permissionStateProvider.setPermissionGranted() + // Simulate event from NotificationsOptInNode.Callback.onNotificationsOptInFinished + service.updateFtueStep() + // Entering PIN code + assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.LockscreenSetup)) + lockScreenService.setIsPinSetup(true) + // Simulate event from LockScreenEntryPoint.Callback.onSetupDone() + service.updateFtueStep() + // Analytics opt in + assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn)) + analyticsService.setDidAskUserConsent() + // Final step + assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete) + } } @Test @@ -158,10 +156,13 @@ class DefaultFtueServiceTest { permissionStateProvider.setPermissionGranted() lockScreenService.setIsPinSetup(true) - assertThat(service.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn) - - analyticsService.setDidAskUserConsent() - assertThat(service.getNextStep(null)).isNull() + service.ftueStepStateFlow.test { + assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown) + // Analytics opt in + assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn)) + analyticsService.setDidAskUserConsent() + assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete) + } } @Test @@ -180,68 +181,30 @@ class DefaultFtueServiceTest { sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified) lockScreenService.setIsPinSetup(true) - assertThat(service.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn) - - analyticsService.setDidAskUserConsent() - assertThat(service.getNextStep(null)).isNull() + service.ftueStepStateFlow.test { + assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown) + // Analytics opt in + assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn)) + analyticsService.setDidAskUserConsent() + assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete) + } } - - @Test - fun `reset do the expected actions S`() = runTest { - val resetAnalyticsLambda = lambdaRecorder { } - val analyticsService = FakeAnalyticsService( - resetLambda = resetAnalyticsLambda - ) - val resetPermissionLambda = lambdaRecorder { } - val permissionStateProvider = FakePermissionStateProvider( - resetPermissionLambda = resetPermissionLambda - ) - val service = createDefaultFtueService( - sdkIntVersion = Build.VERSION_CODES.S, - permissionStateProvider = permissionStateProvider, - analyticsService = analyticsService, - ) - service.reset() - resetAnalyticsLambda.assertions().isCalledOnce() - resetPermissionLambda.assertions().isNeverCalled() - } - - @Test - fun `reset do the expected actions TIRAMISU`() = runTest { - val resetLambda = lambdaRecorder { } - val analyticsService = FakeAnalyticsService( - resetLambda = resetLambda - ) - val resetPermissionLambda = lambdaRecorder { } - val permissionStateProvider = FakePermissionStateProvider( - resetPermissionLambda = resetPermissionLambda - ) - val service = createDefaultFtueService( - sdkIntVersion = Build.VERSION_CODES.TIRAMISU, - permissionStateProvider = permissionStateProvider, - analyticsService = analyticsService, - ) - service.reset() - resetLambda.assertions().isCalledOnce() - resetPermissionLambda.assertions().isCalledOnce() - .with(value("android.permission.POST_NOTIFICATIONS")) - } - - private fun TestScope.createDefaultFtueService( - sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(), - analyticsService: AnalyticsService = FakeAnalyticsService(), - permissionStateProvider: PermissionStateProvider = FakePermissionStateProvider(permissionGranted = false), - lockScreenService: LockScreenService = FakeLockScreenService(), - sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), - // First version where notification permission is required - sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU, - ) = DefaultFtueService( - sessionCoroutineScope = backgroundScope, - sessionVerificationService = sessionVerificationService, - sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion), - analyticsService = analyticsService, - permissionStateProvider = permissionStateProvider, - lockScreenService = lockScreenService, - sessionPreferencesStore = sessionPreferencesStore, - ) } + +internal fun TestScope.createDefaultFtueService( + sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(), + analyticsService: AnalyticsService = FakeAnalyticsService(), + permissionStateProvider: PermissionStateProvider = FakePermissionStateProvider(permissionGranted = false), + lockScreenService: LockScreenService = FakeLockScreenService(), + sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), + // First version where notification permission is required + sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU, +) = DefaultFtueService( + sessionCoroutineScope = backgroundScope, + sessionVerificationService = sessionVerificationService, + sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion), + analyticsService = analyticsService, + permissionStateProvider = permissionStateProvider, + lockScreenService = lockScreenService, + sessionPreferencesStore = sessionPreferencesStore, +) diff --git a/features/ftue/test/build.gradle.kts b/features/ftue/test/build.gradle.kts index 396c7467b6..9c0d4ffadd 100644 --- a/features/ftue/test/build.gradle.kts +++ b/features/ftue/test/build.gradle.kts @@ -1,5 +1,3 @@ -import extension.setupAnvil - /* * Copyright 2024 New Vector Ltd. * @@ -16,8 +14,6 @@ android { namespace = "io.element.android.features.ftue.test" } -setupAnvil() - dependencies { implementation(projects.features.ftue.api) implementation(projects.tests.testutils) diff --git a/features/ftue/test/src/main/kotlin/io/element/android/features/ftue/test/FakeFtueService.kt b/features/ftue/test/src/main/kotlin/io/element/android/features/ftue/test/FakeFtueService.kt index 9217dbd22c..1dbc2c281b 100644 --- a/features/ftue/test/src/main/kotlin/io/element/android/features/ftue/test/FakeFtueService.kt +++ b/features/ftue/test/src/main/kotlin/io/element/android/features/ftue/test/FakeFtueService.kt @@ -9,18 +9,11 @@ package io.element.android.features.ftue.test import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueState -import io.element.android.tests.testutils.lambda.lambdaError import kotlinx.coroutines.flow.MutableStateFlow -class FakeFtueService( - private val resetLambda: () -> Unit = { lambdaError() }, -) : FtueService { +class FakeFtueService : FtueService { override val state: MutableStateFlow = MutableStateFlow(FtueState.Unknown) - override suspend fun reset() { - resetLambda() - } - suspend fun emitState(newState: FtueState) { state.emit(newState) } 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 index 88b55d4e02..9beb147568 100644 --- 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 @@ -28,6 +28,5 @@ interface HomeEntryPoint : FeatureEntryPoint { 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 index 972c0817e2..2bf09c9398 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -22,7 +23,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.appconfig) @@ -38,7 +39,7 @@ dependencies { implementation(projects.libraries.dateformatter.api) implementation(projects.libraries.eventformatter.api) implementation(projects.libraries.indicator.api) - implementation(projects.libraries.deeplink) + implementation(projects.libraries.deeplink.api) implementation(projects.libraries.fullscreenintent.api) implementation(projects.libraries.permissions.api) implementation(projects.libraries.permissions.noop) @@ -55,16 +56,10 @@ dependencies { implementation(libs.haze.materials) implementation(projects.features.reportroom.api) implementation(projects.features.changeroommemberroles.api) + implementation(projects.libraries.previewutils) 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) + testCommonDependencies(libs, true) testImplementation(projects.features.invite.test) testImplementation(projects.features.logout.test) testImplementation(projects.features.networkmonitor.test) @@ -76,8 +71,8 @@ dependencies { testImplementation(projects.libraries.permissions.noop) testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.preferences.test) + testImplementation(projects.libraries.sessionStorage.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/CurrentUserWithNeighborsBuilder.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilder.kt new file mode 100644 index 0000000000..d19222d44f --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilder.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 io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.sessionstorage.api.SessionData +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList + +class CurrentUserWithNeighborsBuilder { + /** + * Build a list of [MatrixUser] containing the current user. If there are other sessions, the list + * will contain 3 users, with the current user in the middle. + * If there is only one other session, the list will contain twice the other user, to allow cycling. + */ + fun build( + matrixUser: MatrixUser, + sessions: List, + ): ImmutableList { + // Sort by position to always have the same order (not depending on last account usage) + return sessions.sortedBy { it.position } + .map { + if (it.userId == matrixUser.userId.value) { + // Always use the freshest profile for the current user + matrixUser + } else { + // Use the data from the DB + MatrixUser( + userId = UserId(it.userId), + displayName = it.userDisplayName, + avatarUrl = it.userAvatarUrl, + ) + } + } + .let { sessionList -> + // If the list has one item, there is no other session, return the list + when (sessionList.size) { + // Can happen when the user signs out (?) + 0 -> listOf(matrixUser) + 1 -> sessionList + else -> { + // Create a list with extra item at the start and end if necessary to have the current user in the middle + // If the list is [A, B, C, D] and the current user is A we want to return [D, A, B] + // If the current user is B, we want to return [A, B, C] + // If the current user is C, we want to return [B, C, D] + // If the current user is D, we want to return [C, D, A] + // Special case: if there are only two users, we want to return [B, A, B] or [A, B, A] to allows cycling + // between the two users. + val currentUserIndex = sessionList.indexOfFirst { it.userId == matrixUser.userId } + when (currentUserIndex) { + // This can happen when the user signs out. + // In this case, just return a singleton list with the current user. + -1 -> listOf(matrixUser) + 0 -> listOf(sessionList.last()) + sessionList.take(2) + sessionList.lastIndex -> sessionList.takeLast(2) + sessionList.first() + else -> sessionList.slice(currentUserIndex - 1..currentUserIndex + 1) + } + } + } + } + .toPersistentList() + } +} 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 index 3fe5c533d7..272a9bc9b1 100644 --- 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 @@ -10,14 +10,15 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 { +@Inject +class DefaultHomeEntryPoint : HomeEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): HomeEntryPoint.NodeBuilder { val plugins = ArrayList() 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 index 4632e40d5a..bc0f821845 100644 --- 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 @@ -7,6 +7,9 @@ package io.element.android.features.home.impl +import io.element.android.libraries.matrix.api.core.SessionId + sealed interface HomeEvents { data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvents + data class SwitchToAccount(val sessionId: SessionId) : 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 index d08abb6891..94f243b634 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt @@ -25,10 +25,10 @@ import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType import io.element.android.features.home.api.HomeEntryPoint @@ -44,7 +44,7 @@ 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.architecture.appyx.launchMolecule -import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase +import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -56,7 +56,8 @@ import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -class HomeFlowNode @AssistedInject constructor( +@AssistedInject +class HomeFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val matrixClient: MatrixClient, @@ -163,7 +164,7 @@ class HomeFlowNode @AssistedInject constructor( stateFlow.value.roomListState.eventSink(RoomListEvents.LeaveRoom(roomId, needsConfirmation = false)) } - fun rootNode(buildContext: BuildContext): Node { + private fun rootNode(buildContext: BuildContext): Node { return node(buildContext) { modifier -> val state by stateFlow.collectAsState() val activity = requireNotNull(LocalActivity.current) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt index c5eea40796..653a7134f3 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt @@ -14,9 +14,12 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.home.impl.roomlist.RoomListState +import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.libraries.architecture.Presenter @@ -27,24 +30,41 @@ 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 +import io.element.android.libraries.sessionstorage.api.SessionStore +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch -class HomePresenter @Inject constructor( +@Inject +class HomePresenter( private val client: MatrixClient, private val syncService: SyncService, private val snackbarDispatcher: SnackbarDispatcher, private val indicatorService: IndicatorService, private val roomListPresenter: Presenter, + private val homeSpacesPresenter: Presenter, private val logoutPresenter: Presenter, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val featureFlagService: FeatureFlagService, + private val sessionStore: SessionStore, ) : Presenter { + private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder() + @Composable override fun present(): HomeState { - val matrixUser = client.userProfile.collectAsState() + val coroutineState = rememberCoroutineScope() + val matrixUser by client.userProfile.collectAsState() + val currentUserAndNeighbors by remember { + combine( + client.userProfile, + sessionStore.sessionsFlow(), + currentUserWithNeighborsBuilder::build, + ) + }.collectAsState(initial = persistentListOf(matrixUser)) val isOnline by syncService.isOnline.collectAsState() val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false) val roomListState = roomListPresenter.present() + val homeSpacesState = homeSpacesPresenter.present() val isSpaceFeatureEnabled by remember { featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space) }.collectAsState(initial = false) @@ -67,16 +87,26 @@ class HomePresenter @Inject constructor( is HomeEvents.SelectHomeNavigationBarItem -> { currentHomeNavigationBarItemOrdinal = event.item.ordinal } + is HomeEvents.SwitchToAccount -> coroutineState.launch { + sessionStore.setLatestSession(event.sessionId.value) + } } } + LaunchedEffect(homeSpacesState.spaceRooms.isEmpty()) { + // If the last space is left, ensure that the Chat view is rendered. + if (homeSpacesState.spaceRooms.isEmpty()) { + currentHomeNavigationBarItemOrdinal = HomeNavigationBarItem.Chats.ordinal + } + } val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() return HomeState( - matrixUser = matrixUser.value, + currentUserAndNeighbors = currentUserAndNeighbors, showAvatarIndicator = showAvatarIndicator, hasNetworkConnection = isOnline, currentHomeNavigationBarItem = currentHomeNavigationBarItem, roomListState = roomListState, + homeSpacesState = homeSpacesState, snackbarMessage = snackbarMessage, canReportBug = canReportBug, directLogoutState = directLogoutState, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt index 5e6c16d2e4..d35412734f 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt @@ -9,17 +9,24 @@ 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.home.impl.spaces.HomeSpacesState 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 +import kotlinx.collections.immutable.ImmutableList @Immutable data class HomeState( - val matrixUser: MatrixUser, + /** + * The current user of this session, in case of multiple accounts, will contains 3 items, with the + * current user in the middle. + */ + val currentUserAndNeighbors: ImmutableList, val showAvatarIndicator: Boolean, val hasNetworkConnection: Boolean, val currentHomeNavigationBarItem: HomeNavigationBarItem, val roomListState: RoomListState, + val homeSpacesState: HomeSpacesState, val snackbarMessage: SnackbarMessage?, val canReportBug: Boolean, val directLogoutState: DirectLogoutState, @@ -27,4 +34,5 @@ data class HomeState( val eventSink: (HomeEvents) -> Unit, ) { val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats + val showNavigationBar = isSpaceFeatureEnabled && homeSpacesState.spaceRooms.isNotEmpty() } 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 index 7a50296e17..7ada259e08 100644 --- 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 @@ -13,12 +13,15 @@ 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.home.impl.spaces.HomeSpacesState +import io.element.android.features.home.impl.spaces.aHomeSpacesState 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 +import kotlinx.collections.immutable.toPersistentList open class HomeStateProvider : PreviewParameterProvider { override val values: Sequence @@ -34,6 +37,8 @@ open class HomeStateProvider : PreviewParameterProvider { summaries = generateRoomListRoomSummaryList(), ) ), + // For the bottom nav bar to be visible in the preview, the user must be member of at least one space + homeSpacesState = aHomeSpacesState(), ), aHomeState( isSpaceFeatureEnabled = true, @@ -46,17 +51,19 @@ open class HomeStateProvider : PreviewParameterProvider { internal fun aHomeState( matrixUser: MatrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"), + currentUserAndNeighbors: List = listOf(matrixUser), showAvatarIndicator: Boolean = false, hasNetworkConnection: Boolean = true, snackbarMessage: SnackbarMessage? = null, currentHomeNavigationBarItem: HomeNavigationBarItem = HomeNavigationBarItem.Chats, roomListState: RoomListState = aRoomListState(), + homeSpacesState: HomeSpacesState = aHomeSpacesState(), canReportBug: Boolean = true, isSpaceFeatureEnabled: Boolean = false, directLogoutState: DirectLogoutState = aDirectLogoutState(), eventSink: (HomeEvents) -> Unit = {} ) = HomeState( - matrixUser = matrixUser, + currentUserAndNeighbors = currentUserAndNeighbors.toPersistentList(), showAvatarIndicator = showAvatarIndicator, hasNetworkConnection = hasNetworkConnection, snackbarMessage = snackbarMessage, @@ -64,6 +71,7 @@ internal fun aHomeState( directLogoutState = directLogoutState, currentHomeNavigationBarItem = currentHomeNavigationBarItem, roomListState = roomListState, + homeSpacesState = homeSpacesState, 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 index c0e999e7f5..aa4742f074 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt @@ -25,12 +25,12 @@ 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.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.hazeEffect @@ -49,6 +49,7 @@ 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.home.impl.spaces.HomeSpacesView 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 @@ -60,7 +61,6 @@ import io.element.android.libraries.designsystem.theme.components.NavigationBarI import io.element.android.libraries.designsystem.theme.components.NavigationBarItem import io.element.android.libraries.designsystem.theme.components.NavigationBarText 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 @@ -171,12 +171,15 @@ private fun HomeScaffold( topBar = { RoomListTopBar( title = stringResource(state.currentHomeNavigationBarItem.labelRes), - matrixUser = state.matrixUser, + currentUserAndNeighbors = state.currentUserAndNeighbors, showAvatarIndicator = state.showAvatarIndicator, areSearchResultsDisplayed = roomListState.searchState.isSearchActive, onToggleSearch = { roomListState.eventSink(RoomListEvents.ToggleSearchResults) }, onMenuActionClick = onMenuActionClick, onOpenSettings = onOpenSettings, + onAccountSwitch = { + state.eventSink(HomeEvents.SwitchToAccount(it)) + }, scrollBehavior = scrollBehavior, displayMenuItems = state.displayActions, displayFilters = roomListState.displayFilters && state.currentHomeNavigationBarItem == HomeNavigationBarItem.Chats, @@ -194,7 +197,7 @@ private fun HomeScaffold( ) }, bottomBar = { - if (state.isSpaceFeatureEnabled) { + if (state.showNavigationBar) { NavigationBar( containerColor = Color.Transparent, modifier = Modifier @@ -261,19 +264,17 @@ private fun HomeScaffold( ) } HomeNavigationBarItem.Spaces -> { - Box( + HomeSpacesView( 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, - ) - } + .hazeSource(state = hazeState), + state = state.homeSpacesState, + onSpaceClick = { spaceId -> + onRoomClick(spaceId) + } + ) } } }, @@ -313,3 +314,22 @@ internal fun HomeViewPreview(@PreviewParameter(HomeStateProvider::class) state: leaveRoomView = {} ) } + +@Preview +@Composable +internal fun HomeViewA11yPreview() = ElementPreview { + HomeView( + homeState = aHomeState(), + onRoomClick = {}, + onSettingsClick = {}, + onSetUpRecoveryClick = {}, + onConfirmRecoveryKeyClick = {}, + onStartChatClick = {}, + onRoomSettingsClick = {}, + onReportRoomClick = {}, + onMenuActionClick = {}, + onDeclineInviteAndBlockUser = {}, + acceptDeclineInviteView = {}, + leaveRoomView = {} + ) +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt index f1f06afe6d..212ba6f29b 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt @@ -11,19 +11,25 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.pager.VerticalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -41,7 +47,6 @@ 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.modifiers.backgroundVerticalGradient @@ -57,23 +62,29 @@ 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.MediumTopAppBar import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList @OptIn(ExperimentalMaterial3Api::class) @Composable fun RoomListTopBar( title: String, - matrixUser: MatrixUser, + currentUserAndNeighbors: ImmutableList, showAvatarIndicator: Boolean, areSearchResultsDisplayed: Boolean, onToggleSearch: () -> Unit, onMenuActionClick: (RoomListMenuAction) -> Unit, onOpenSettings: () -> Unit, + onAccountSwitch: (SessionId) -> Unit, scrollBehavior: TopAppBarScrollBehavior, displayMenuItems: Boolean, displayFilters: Boolean, @@ -83,10 +94,11 @@ fun RoomListTopBar( ) { DefaultRoomListTopBar( title = title, - matrixUser = matrixUser, + currentUserAndNeighbors = currentUserAndNeighbors, showAvatarIndicator = showAvatarIndicator, areSearchResultsDisplayed = areSearchResultsDisplayed, onOpenSettings = onOpenSettings, + onAccountSwitch = onAccountSwitch, onSearchClick = onToggleSearch, onMenuActionClick = onMenuActionClick, scrollBehavior = scrollBehavior, @@ -102,11 +114,12 @@ fun RoomListTopBar( @Composable private fun DefaultRoomListTopBar( title: String, - matrixUser: MatrixUser, + currentUserAndNeighbors: ImmutableList, showAvatarIndicator: Boolean, areSearchResultsDisplayed: Boolean, scrollBehavior: TopAppBarScrollBehavior, onOpenSettings: () -> Unit, + onAccountSwitch: (SessionId) -> Unit, onSearchClick: () -> Unit, onMenuActionClick: (RoomListMenuAction) -> Unit, displayMenuItems: Boolean, @@ -116,12 +129,6 @@ private fun DefaultRoomListTopBar( modifier: Modifier = Modifier, ) { val collapsedFraction = scrollBehavior.state.collapsedFraction - val avatarData by remember(matrixUser) { - derivedStateOf { - matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar) - } - } - Box(modifier = modifier) { val collapsedTitleTextStyle = ElementTheme.typography.aliasScreenTitle val expandedTitleTextStyle = ElementTheme.typography.fontHeadingLgBold.copy( @@ -158,8 +165,9 @@ private fun DefaultRoomListTopBar( }, navigationIcon = { NavigationIcon( - avatarData = avatarData, + currentUserAndNeighbors = currentUserAndNeighbors, showAvatarIndicator = showAvatarIndicator, + onAccountSwitch = onAccountSwitch, onClick = onOpenSettings, ) }, @@ -247,19 +255,67 @@ private fun DefaultRoomListTopBar( @Composable private fun NavigationIcon( - avatarData: AvatarData, + currentUserAndNeighbors: ImmutableList, + showAvatarIndicator: Boolean, + onAccountSwitch: (SessionId) -> Unit, + onClick: () -> Unit, +) { + if (currentUserAndNeighbors.size == 1) { + AccountIcon( + matrixUser = currentUserAndNeighbors.single(), + isCurrentAccount = true, + showAvatarIndicator = showAvatarIndicator, + onClick = onClick, + ) + } else { + // Render a vertical pager + val pagerState = rememberPagerState(initialPage = 1) { currentUserAndNeighbors.size } + // Listen to page changes and switch account if needed + val latestOnAccountSwitch by rememberUpdatedState(onAccountSwitch) + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.settledPage }.collect { page -> + latestOnAccountSwitch(SessionId(currentUserAndNeighbors[page].userId.value)) + } + } + VerticalPager( + state = pagerState, + modifier = Modifier.height(48.dp), + ) { page -> + AccountIcon( + matrixUser = currentUserAndNeighbors[page], + isCurrentAccount = page == 1, + showAvatarIndicator = page == 1 && showAvatarIndicator, + onClick = if (page == 1) { + onClick + } else { + {} + }, + ) + } + } +} + +@Composable +private fun AccountIcon( + matrixUser: MatrixUser, + isCurrentAccount: Boolean, showAvatarIndicator: Boolean, onClick: () -> Unit, ) { IconButton( - modifier = Modifier.testTag(TestTags.homeScreenSettings), + modifier = if (isCurrentAccount) Modifier.testTag(TestTags.homeScreenSettings) else Modifier, onClick = onClick, ) { Box { + val avatarData by remember(matrixUser) { + derivedStateOf { + matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar) + } + } Avatar( avatarData = avatarData, avatarType = AvatarType.User, - contentDescription = stringResource(CommonStrings.common_settings), + contentDescription = if (isCurrentAccount) stringResource(CommonStrings.common_settings) else null, ) if (showAvatarIndicator) { RedIndicatorAtom( @@ -276,11 +332,12 @@ private fun NavigationIcon( internal fun DefaultRoomListTopBarPreview() = ElementPreview { DefaultRoomListTopBar( title = stringResource(R.string.screen_roomlist_main_space_title), - matrixUser = MatrixUser(UserId("@id:domain"), "Alice"), + currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), showAvatarIndicator = false, areSearchResultsDisplayed = false, scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()), onOpenSettings = {}, + onAccountSwitch = {}, onSearchClick = {}, displayMenuItems = true, displayFilters = true, @@ -296,11 +353,33 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview { internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview { DefaultRoomListTopBar( title = stringResource(R.string.screen_roomlist_main_space_title), - matrixUser = MatrixUser(UserId("@id:domain"), "Alice"), + currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), showAvatarIndicator = true, areSearchResultsDisplayed = false, scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()), onOpenSettings = {}, + onAccountSwitch = {}, + onSearchClick = {}, + displayMenuItems = true, + displayFilters = true, + filtersState = aRoomListFiltersState(), + canReportBug = true, + onMenuActionClick = {}, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@PreviewsDayNight +@Composable +internal fun DefaultRoomListTopBarMultiAccountPreview() = ElementPreview { + DefaultRoomListTopBar( + title = stringResource(R.string.screen_roomlist_main_space_title), + currentUserAndNeighbors = aMatrixUserList().take(3).toPersistentList(), + showAvatarIndicator = false, + areSearchResultsDisplayed = false, + scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()), + onOpenSettings = {}, + onAccountSwitch = {}, onSearchClick = {}, displayMenuItems = true, displayFilters = true, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt index a065da16e5..3036865eea 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter 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 @@ -44,15 +45,13 @@ 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.atomic.molecules.InviteButtonsRowMolecule import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.Button -import io.element.android.libraries.designsystem.theme.components.ButtonSize import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.roomListRoomMessage import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate @@ -100,7 +99,7 @@ internal fun RoomSummaryRow( ) } Spacer(modifier = Modifier.height(12.dp)) - InviteButtonsRow( + InviteButtonsRowMolecule( onAcceptClick = { eventSink(RoomListEvents.AcceptInvite(room)) }, @@ -285,9 +284,13 @@ private fun MessagePreviewAndIndicatorRow( maxLines = 2, overflow = TextOverflow.Ellipsis ) + // Call and unread Row( - modifier = Modifier.height(16.dp), + modifier = Modifier + .height(16.dp) + // Used to force this line to be read aloud earlier than the latest event when using Talkback + .zIndex(-1f), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { @@ -303,8 +306,10 @@ private fun MessagePreviewAndIndicatorRow( MentionIndicatorAtom() } if (room.hasNewContent) { + val contentDescription = stringResource(CommonStrings.a11y_notifications_new_messages) UnreadIndicatorAtom( - color = tint + color = tint, + contentDescription = contentDescription, ) } } @@ -339,31 +344,6 @@ private fun InviteNameAndIndicatorRow( } } -@Composable -private fun InviteButtonsRow( - onAcceptClick: () -> Unit, - onDeclineClick: () -> Unit, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier, - horizontalArrangement = spacedBy(12.dp) - ) { - OutlinedButton( - text = stringResource(CommonStrings.action_decline), - onClick = onDeclineClick, - size = ButtonSize.MediumLowPadding, - modifier = Modifier.weight(1f), - ) - Button( - text = stringResource(CommonStrings.action_accept), - onClick = onAcceptClick, - size = ButtonSize.MediumLowPadding, - modifier = Modifier.weight(1f), - ) - } -} - @Composable private fun OnGoingCallIcon( color: Color, @@ -371,7 +351,7 @@ private fun OnGoingCallIcon( Icon( modifier = Modifier.size(16.dp), imageVector = CompoundIcons.VideoCallSolid(), - contentDescription = null, + contentDescription = stringResource(CommonStrings.a11y_notifications_ongoing_call), tint = color, ) } @@ -380,7 +360,7 @@ private fun OnGoingCallIcon( private fun NotificationOffIndicatorAtom() { Icon( modifier = Modifier.size(16.dp), - contentDescription = null, + contentDescription = stringResource(CommonStrings.a11y_notifications_muted), imageVector = CompoundIcons.NotificationsOffSolid(), tint = ElementTheme.colors.iconQuaternary, ) @@ -390,7 +370,7 @@ private fun NotificationOffIndicatorAtom() { private fun MentionIndicatorAtom() { Icon( modifier = Modifier.size(16.dp), - contentDescription = null, + contentDescription = stringResource(CommonStrings.a11y_notifications_new_mentions), imageVector = CompoundIcons.Mention(), tint = ElementTheme.colors.unreadIndicator, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt index 3ed7c383ba..b9e8d27bf7 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt @@ -7,6 +7,7 @@ package io.element.android.features.home.impl.datasource +import dev.zacsweers.metro.Inject 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 @@ -29,10 +30,11 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import javax.inject.Inject +import timber.log.Timber import kotlin.time.Duration.Companion.seconds -class RoomListDataSource @Inject constructor( +@Inject +class RoomListDataSource( private val roomListService: RoomListService, private val roomListRoomSummaryFactory: RoomListRoomSummaryFactory, private val coroutineDispatchers: CoroutineDispatchers, @@ -101,13 +103,44 @@ class RoomListDataSource @Inject constructor( } private suspend fun buildAndEmitAllRooms(roomSummaries: List, useCache: Boolean = true) { + // Used to detect duplicates in the room list summaries - see comment below + data class CacheResult(val index: Int, val fromCache: Boolean) + val cachingResults = mutableMapOf>() + val roomListRoomSummaries = diffCache.indices().mapNotNull { index -> if (useCache) { - diffCache.get(index) ?: buildAndCacheItem(roomSummaries, index) + diffCache.get(index)?.let { cachedItem -> + // Add the cached item to the caching results + val pairs = cachingResults.getOrDefault(cachedItem.roomId, mutableListOf()) + pairs.add(CacheResult(index, fromCache = true)) + cachingResults[cachedItem.roomId] = pairs + cachedItem + } ?: run { + roomSummaries.getOrNull(index)?.roomId?.let { + // Add the non-cached item to the caching results + val pairs = cachingResults.getOrDefault(it, mutableListOf()) + pairs.add(CacheResult(index, fromCache = false)) + cachingResults[it] = pairs + } + buildAndCacheItem(roomSummaries, index) + } } else { + roomSummaries.getOrNull(index)?.roomId?.let { + // Add the non-cached item to the caching results + val pairs = cachingResults.getOrDefault(it, mutableListOf()) + pairs.add(CacheResult(index, fromCache = false)) + cachingResults[it] = pairs + } buildAndCacheItem(roomSummaries, index) } } + + // TODO remove once https://github.com/element-hq/element-x-android/issues/5031 has been confirmed as fixed + val duplicates = cachingResults.filter { (_, operations) -> operations.size > 1 } + if (duplicates.isNotEmpty()) { + Timber.e("Found duplicates in room summaries after an UI update: $duplicates. This could be a race condition/caching issue of some kind") + } + _allRooms.emit(roomListRoomSummaries.toImmutableList()) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt index 3d77662188..b6f908fd5d 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt @@ -7,6 +7,7 @@ package io.element.android.features.home.impl.datasource +import dev.zacsweers.metro.Inject 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 @@ -20,9 +21,9 @@ import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.toInviteSender import kotlinx.collections.immutable.toImmutableList -import javax.inject.Inject -class RoomListRoomSummaryFactory @Inject constructor( +@Inject +class RoomListRoomSummaryFactory( private val dateFormatter: DateFormatter, private val roomLastMessageFormatter: RoomLastMessageFormatter, ) { diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesModule.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesModule.kt new file mode 100644 index 0000000000..e8631431ca --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesModule.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.home.impl.di + +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo +import io.element.android.features.home.impl.spaces.HomeSpacesPresenter +import io.element.android.features.home.impl.spaces.HomeSpacesState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.SessionScope + +@BindingContainer +@ContributesTo(SessionScope::class) +interface HomeSpacesModule { + @Binds + fun bindHomeSpacesPresenter(presenter: HomeSpacesPresenter): Presenter +} 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 index ef64512462..926205cbea 100644 --- 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 @@ -7,9 +7,9 @@ package io.element.android.features.home.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo 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 @@ -20,7 +20,7 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.SessionScope @ContributesTo(SessionScope::class) -@Module +@BindingContainer interface RoomListModule { @Binds fun bindRoomListPresenter(presenter: RoomListPresenter): Presenter diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt index de3f7eaa0b..07d2de96a1 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt @@ -10,15 +10,16 @@ package io.element.android.features.home.impl.filters import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState +import dev.zacsweers.metro.Inject 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 import kotlinx.coroutines.flow.map -import javax.inject.Inject import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter -class RoomListFiltersPresenter @Inject constructor( +@Inject +class RoomListFiltersPresenter( private val roomListService: RoomListService, private val filterSelectionStrategy: FilterSelectionStrategy, ) : Presenter { diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt index 26a77da5c7..c1da8b18b2 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt @@ -7,14 +7,15 @@ package io.element.android.features.home.impl.filters.selection -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 @ContributesBinding(SessionScope::class) -class DefaultFilterSelectionStrategy @Inject constructor() : FilterSelectionStrategy { +@Inject +class DefaultFilterSelectionStrategy : FilterSelectionStrategy { private val selectedFilters = LinkedHashSet() override val filterSelectionStates = MutableStateFlow(buildFilters()) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt index ba5007c57c..b8e299c5e9 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow +import dev.zacsweers.metro.Inject 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 @@ -35,7 +36,6 @@ import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.core.coroutine.mapState 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 @@ -43,6 +43,7 @@ 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.matrix.ui.safety.rememberHideInvitesAvatar import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.push.api.battery.BatteryOptimizationState @@ -63,12 +64,12 @@ 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( +@Inject +class RoomListPresenter( private val client: MatrixClient, private val leaveRoomPresenter: Presenter, private val roomListDataSource: RoomListDataSource, @@ -100,12 +101,7 @@ class RoomListPresenter @Inject constructor( var securityBannerDismissed by rememberSaveable { mutableStateOf(false) } // Avatar indicator - val hideInvitesAvatar by remember { - client - .mediaPreviewService() - .mediaPreviewConfigFlow - .mapState { config -> config.hideInviteAvatar } - }.collectAsState() + val hideInvitesAvatar by client.rememberHideInvitesAvatar() val contextMenu = remember { mutableStateOf(RoomListState.ContextMenu.Hidden) } val declineInviteMenu = remember { mutableStateOf(RoomListState.DeclineInviteMenu.Hidden) } 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 index 55fc8948f6..089e5c23a5 100644 --- 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 @@ -16,14 +16,12 @@ 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.invite.api.acceptdecline.anAcceptDeclineInviteState import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState -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 @@ -76,16 +74,6 @@ internal fun aLeaveRoomState( override val eventSink: (LeaveRoomEvent) -> Unit = 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( diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt index 6840809480..4cb4104637 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt @@ -7,6 +7,7 @@ package io.element.android.features.home.impl.search +import dev.zacsweers.metro.Inject 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 @@ -20,11 +21,11 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import javax.inject.Inject private const val PAGE_SIZE = 30 -class RoomListSearchDataSource @Inject constructor( +@Inject +class RoomListSearchDataSource( roomListService: RoomListService, coroutineDispatchers: CoroutineDispatchers, private val roomSummaryFactory: RoomListRoomSummaryFactory, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt index b4d151fe02..ba77b0cdce 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt @@ -14,11 +14,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter import kotlinx.collections.immutable.persistentListOf -import javax.inject.Inject -class RoomListSearchPresenter @Inject constructor( +@Inject +class RoomListSearchPresenter( private val dataSource: RoomListSearchDataSource, ) : Presenter { @Composable diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/AppScope.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvents.kt similarity index 55% rename from libraries/di/src/main/kotlin/io/element/android/libraries/di/AppScope.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvents.kt index a1015055bf..5d07a5e358 100644 --- a/libraries/di/src/main/kotlin/io/element/android/libraries/di/AppScope.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvents.kt @@ -1,10 +1,10 @@ /* - * Copyright 2022-2024 New Vector Ltd. + * Copyright 2025 New Vector Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.di +package io.element.android.features.home.impl.spaces -abstract class AppScope private constructor() +sealed interface HomeSpacesEvents diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt new file mode 100644 index 0000000000..dea6defc0a --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.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.home.impl.spaces + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import dev.zacsweers.metro.Inject +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toPersistentSet +import kotlinx.coroutines.flow.map + +@Inject +class HomeSpacesPresenter( + private val client: MatrixClient, + private val seenInvitesStore: SeenInvitesStore, +) : Presenter { + @Composable + override fun present(): HomeSpacesState { + val hideInvitesAvatar by client.rememberHideInvitesAvatar() + val spaceRooms by client.spaceService.spaceRoomsFlow.collectAsState(emptyList()) + val seenSpaceInvites by remember { + seenInvitesStore.seenRoomIds().map { it.toPersistentSet() } + }.collectAsState(persistentSetOf()) + + fun handleEvents(event: HomeSpacesEvents) { + // when (event) { } + } + + return HomeSpacesState( + space = CurrentSpace.Root, + spaceRooms = spaceRooms, + seenSpaceInvites = seenSpaceInvites, + hideInvitesAvatar = hideInvitesAvatar, + eventSink = ::handleEvents, + ) + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt new file mode 100644 index 0000000000..96733991f9 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.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.home.impl.spaces + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import kotlinx.collections.immutable.ImmutableSet + +data class HomeSpacesState( + val space: CurrentSpace, + val spaceRooms: List, + val seenSpaceInvites: ImmutableSet, + val hideInvitesAvatar: Boolean, + val eventSink: (HomeSpacesEvents) -> Unit, +) + +sealed interface CurrentSpace { + object Root : CurrentSpace + data class Space(val spaceRoom: SpaceRoom) : CurrentSpace +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt new file mode 100644 index 0000000000..921c340886 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.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.home.impl.spaces + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.previewutils.room.aSpaceRoom +import kotlinx.collections.immutable.toImmutableSet + +open class HomeSpacesStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aHomeSpacesState( + spaceRooms = SpaceRoomProvider().values.toList(), + seenSpaceInvites = setOf( + RoomId("!spaceId3:example.com"), + ), + ), + aHomeSpacesState( + space = CurrentSpace.Space( + spaceRoom = aSpaceRoom(roomId = RoomId("!mySpace:example.com")) + ), + spaceRooms = aListOfSpaceRooms(), + ), + ) +} + +internal fun aHomeSpacesState( + space: CurrentSpace = CurrentSpace.Root, + spaceRooms: List = aListOfSpaceRooms(), + seenSpaceInvites: Set = emptySet(), + hideInvitesAvatar: Boolean = false, + eventSink: (HomeSpacesEvents) -> Unit = {}, +) = HomeSpacesState( + space = space, + spaceRooms = spaceRooms, + seenSpaceInvites = seenSpaceInvites.toImmutableSet(), + hideInvitesAvatar = hideInvitesAvatar, + eventSink = eventSink, +) + +fun aListOfSpaceRooms(): List { + return listOf( + aSpaceRoom(roomId = RoomId("!spaceId0:example.com")), + aSpaceRoom(roomId = RoomId("!spaceId1:example.com")), + aSpaceRoom(roomId = RoomId("!spaceId2:example.com")), + ) +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt new file mode 100644 index 0000000000..36e0bbc56a --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.spaces + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView +import io.element.android.libraries.matrix.ui.components.SpaceHeaderView +import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView +import io.element.android.libraries.matrix.ui.model.getAvatarData +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun HomeSpacesView( + state: HomeSpacesState, + onSpaceClick: (RoomId) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn(modifier) { + val space = state.space + when (space) { + CurrentSpace.Root -> { + item { + SpaceHeaderRootView( + numberOfSpaces = state.spaceRooms.size, + // TODO + numberOfRooms = 0, + ) + } + } + is CurrentSpace.Space -> item { + SpaceHeaderView( + avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader), + name = space.spaceRoom.name, + topic = space.spaceRoom.topic, + joinRule = space.spaceRoom.joinRule, + heroes = space.spaceRoom.heroes.toImmutableList(), + numberOfMembers = space.spaceRoom.numJoinedMembers, + numberOfRooms = space.spaceRoom.childrenCount, + ) + } + } + state.spaceRooms.forEach { spaceRoom -> + item(spaceRoom.roomId) { + val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED + SpaceRoomItemView( + spaceRoom = spaceRoom, + showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites, + hideAvatars = isInvitation && state.hideInvitesAvatar, + onClick = { + onSpaceClick(spaceRoom.roomId) + }, + onLongClick = { + // TODO + }, + ) + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun HomeSpacesViewPreview( + @PreviewParameter(HomeSpacesStateProvider::class) state: HomeSpacesState, +) = ElementPreview { + HomeSpacesView( + state = state, + onSpaceClick = {}, + modifier = Modifier, + ) +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt new file mode 100644 index 0000000000..474e08293a --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.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.home.impl.spaces + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +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.spaces.SpaceRoom +import io.element.android.libraries.previewutils.room.aSpaceRoom + +class SpaceRoomProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + aSpaceRoom(), + aSpaceRoom( + numJoinedMembers = 5, + childrenCount = 10, + worldReadable = true, + roomId = RoomId("!spaceId0:example.com"), + ), + aSpaceRoom( + numJoinedMembers = 5, + childrenCount = 10, + worldReadable = true, + avatarUrl = "anUrl", + roomId = RoomId("!spaceId1:example.com"), + ), + aSpaceRoom( + name = null, + numJoinedMembers = 5, + childrenCount = 10, + worldReadable = true, + avatarUrl = "anUrl", + roomId = RoomId("!spaceId2:example.com"), + state = CurrentUserMembership.INVITED, + ), + aSpaceRoom( + name = null, + numJoinedMembers = 5, + childrenCount = 10, + worldReadable = true, + avatarUrl = "anUrl", + roomId = RoomId("!spaceId3:example.com"), + state = CurrentUserMembership.INVITED, + ), + ) +} diff --git a/features/home/impl/src/main/res/values-bg/translations.xml b/features/home/impl/src/main/res/values-bg/translations.xml index b2053200fc..f988aee0fd 100644 --- a/features/home/impl/src/main/res/values-bg/translations.xml +++ b/features/home/impl/src/main/res/values-bg/translations.xml @@ -6,8 +6,12 @@ "Всички чатове" "Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?" "Отказване на покана" + "Сигурни ли сте, че искате да откажете този личен чат с %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 index 4754bddeda..3699b7975e 100644 --- a/features/home/impl/src/main/res/values-cs/translations.xml +++ b/features/home/impl/src/main/res/values-cs/translations.xml @@ -13,6 +13,7 @@ "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í" "Všechny chaty" + "Prostory" "Opravdu chcete odmítnout pozvánku do %1$s?" "Odmítnout pozvání" "Opravdu chcete odmítnout tuto soukromou konverzaci s %1$s?" @@ -32,6 +33,7 @@ Prozatím můžete zrušit výběr filtrů, abyste viděli své další chaty""Pozvánky" "Nemáte žádné nevyřízené pozvánky." "Nízká priorita" + "Zatím nemáte žádné chaty s nízkou prioritou" "Můžete zrušit výběr filtrů, abyste viděli své další chaty" "Nemáte chaty pro tento výběr" "Lidé" diff --git a/features/home/impl/src/main/res/values-cy/translations.xml b/features/home/impl/src/main/res/values-cy/translations.xml index bde8ee4c19..c8417952a6 100644 --- a/features/home/impl/src/main/res/values-cy/translations.xml +++ b/features/home/impl/src/main/res/values-cy/translations.xml @@ -13,6 +13,7 @@ "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" "Sgyrsiau" + "Gofodau" "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?" @@ -32,6 +33,7 @@ Am y tro, gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill""Gwahoddiadau" "Does gennych chi ddim gwahoddiadau yn aros." "Blaenoriaeth Isel" + "Does gennych chi ddim sgyrsiau blaenoriaeth isel eto" "Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill" "Does gennych chi ddim sgyrsiau ar gyfer y dewis hwn" "Pobl" diff --git a/features/home/impl/src/main/res/values-da/translations.xml b/features/home/impl/src/main/res/values-da/translations.xml index 274b5a9d60..b5cc2ef7cb 100644 --- a/features/home/impl/src/main/res/values-da/translations.xml +++ b/features/home/impl/src/main/res/values-da/translations.xml @@ -33,9 +33,10 @@ For nu kan du fravælge filtre for at se dine andre samtaler" "Invitationer" "Du har ingen afventende invitationer." "Lav prioritet" + "Du har endnu ingen chats med lav prioritet" "Du kan fravælge filtre for at se dine andre samtaler" "Du har ingen samtaler til dette valg" - "Mennesker" + "Brugere" "Du har ingen DM\'er endnu" "Rum" "Du er ikke i noget rum endnu" diff --git a/features/home/impl/src/main/res/values-de/translations.xml b/features/home/impl/src/main/res/values-de/translations.xml index a6b2593bc6..0596dc45e4 100644 --- a/features/home/impl/src/main/res/values-de/translations.xml +++ b/features/home/impl/src/main/res/values-de/translations.xml @@ -3,49 +3,51 @@ "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." + "Stelle Deine kryptographische Identität und Deinen Nachrichtenverlauf mit Hilfe eines Wiederherstellungsschlüssels wieder her, falls du alle deine Geräte verloren haben solltest" "Wiederherstellung einrichten" "Wiederherstellung einrichten" - "Bestä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." + "Bestätige deinen Wiederherstellungsschlüssel, um weiterhin auf deinen Schlüsselspeicher und den Nachrichtenverlauf zugreifen zu können." + "Gib deinen Wiederherstellungsschlüssel ein" + "Hast du deinen Wiederherstellungsschlüssel vergessen?" + "Dein 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" "Chats" - "Möchten Sie die Einladung zum Betreten von %1$s wirklich ablehnen?" + "Spaces" + "Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?" "Einladung ablehnen" - "Möchten Sie diesen privaten Chat mit %1$s wirklich 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" "Dies ist ein einmaliger Vorgang, danke fürs Warten." "Dein Konto wird eingerichtet." - "Eine Unterthaltung oder Raum erstellen" + "Einen neuen Chat erstellen" "Filter zurücksetzen" - "Beginnen Sie, indem Sie jemandem eine Nachricht senden." + "Schick einfach jemandem eine Nachricht, um loszulegen." "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" + "In den Chat Einstellungen kannst du einen Chat als Favorit markieren. +Deaktiviere den entsprechenden Filter, um deine anderen Chats zu sehen" + "Du hast noch keine Chats als Favorit markiert" "Einladungen" - "Sie haben keine ausstehenden Einladungen." + "Du hast keine ausstehenden Einladungen." "Niedrige Priorität" - "Wähle Filter ab, um Deine Chats zu sehen." - "Diese Chats entsprechen diesen Kriterien nicht." + "Du hast noch keine Chats mit niedriger Priorität." + "Wähle Filter ab, um Chats zu sehen." + "Für diese Auswahl hast du keinen Chat." "Personen" - "Sie haben noch keine Direktnachrichten" - "Räume" - "Sie sind noch in keinem Raum" + "Du hast noch keine Direktnachrichten" + "Gruppen" + "Du bist noch in keinem Chat" "Ungelesen" "Glückwunsch! -Sie haben keine ungelesenen Nachrichten!" +Du hast 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" + "Die Chat-Version wurde aktualisiert" + "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." + "Verifiziere deine Identität" diff --git a/features/home/impl/src/main/res/values-eo/translations.xml b/features/home/impl/src/main/res/values-eo/translations.xml new file mode 100644 index 0000000000..46986026aa --- /dev/null +++ b/features/home/impl/src/main/res/values-eo/translations.xml @@ -0,0 +1,11 @@ + + + "Restore your account security and message history with a backup password if you have lost all your existing devices." + "Set up backup" + "Set up backup to protect your account" + "Confirm your backup password to maintain access to your message backup and message history." + "Enter your backup password" + "Forgot your backup password?" + "Your message backup is out of sync" + "Looks like you\'re using a new device. Confirm it with another linked device to access your encrypted messages." + diff --git a/features/home/impl/src/main/res/values-et/translations.xml b/features/home/impl/src/main/res/values-et/translations.xml index 39e099adba..43dd57cdfa 100644 --- a/features/home/impl/src/main/res/values-et/translations.xml +++ b/features/home/impl/src/main/res/values-et/translations.xml @@ -33,6 +33,7 @@ Aga seni… oma teiste vestluste nägemiseks pead eemaldama filtrid" "Kutsed" "Sul pole ootel kutseid." "Vähetähtis" + "Sul pole veel ühtegi olulist vestlust" "Oma teiste vestluste nägemiseks sa pead filtrid eemaldama" "Selle valiku jaoks sul veel pole vestlusi" "Inimesed" diff --git a/features/home/impl/src/main/res/values-fi/translations.xml b/features/home/impl/src/main/res/values-fi/translations.xml index 52e8e9b9a2..31b5ff0a1a 100644 --- a/features/home/impl/src/main/res/values-fi/translations.xml +++ b/features/home/impl/src/main/res/values-fi/translations.xml @@ -33,6 +33,7 @@ Toistaiseksi voit poistaa suodattimien valinnan, jotta näet muut keskustelut."< "Kutsut" "Sinulla ei ole yhtään odottavaa kutsua." "Matala prioriteetti" + "Sinulla ei ole vielä yhtään matalan prioriteetin keskustelua" "Voit poistaa suodattimien valinnan nähdäksesi muut keskustelusi." "Sinulla ei ole sopivia keskusteluja tähän valintaan" "Ihmiset" diff --git a/features/home/impl/src/main/res/values-fr/translations.xml b/features/home/impl/src/main/res/values-fr/translations.xml index 540eb3e887..11f842916f 100644 --- a/features/home/impl/src/main/res/values-fr/translations.xml +++ b/features/home/impl/src/main/res/values-fr/translations.xml @@ -33,6 +33,7 @@ En attendant, vous pouvez désélectionner des filtres pour voir vos autres salo "Invitations" "Vous n’avez aucune invitation en attente." "Priorité basse" + "Vous n’avez pas encore de salon à priorité basse" "Veuillez désélectionner des filtres pour voir vos discussions" "Vous n’avez pas de discussions pour cette sélection" "Personnes" diff --git a/features/home/impl/src/main/res/values-hu/translations.xml b/features/home/impl/src/main/res/values-hu/translations.xml index 8d498d08a6..7250794095 100644 --- a/features/home/impl/src/main/res/values-hu/translations.xml +++ b/features/home/impl/src/main/res/values-hu/translations.xml @@ -1,6 +1,6 @@ - "Kapcsolja ki az alkalmazás akkumulátor-optimalizálását, hogy biztosan megkapja az összes értesítést." + "Kapcsolja ki az alkalmazás akkumulátoroptimalizá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." diff --git a/features/home/impl/src/main/res/values-it/translations.xml b/features/home/impl/src/main/res/values-it/translations.xml index d6cfef8db3..74f8d4abac 100644 --- a/features/home/impl/src/main/res/values-it/translations.xml +++ b/features/home/impl/src/main/res/values-it/translations.xml @@ -3,7 +3,7 @@ "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." + "Recupera la tua identità crittografica e la cronologia dei messaggi con una chiave di recupero se hai perso tutti i tuoi dispositivi." "Configura il recupero" "Configura il ripristino" "Conferma la chiave di recupero per mantenere l\'accesso all\'archiviazione delle chiavi e alla cronologia dei messaggi." @@ -13,6 +13,7 @@ "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" "Tutte le conversazioni" + "Spazi" "Vuoi davvero rifiutare l\'invito ad entrare in %1$s?" "Rifiuta l\'invito" "Vuoi davvero rifiutare questa conversazione privata con %1$s?" @@ -32,6 +33,7 @@ Per il momento, puoi deselezionare i filtri per vedere le altre conversazioni."< "Inviti" "Non hai nessun invito in sospeso." "Bassa priorità" + "Non hai ancora conversazioni a bassa priorità" "Puoi deselezionare i filtri per vedere le altre conversazioni." "Non hai conversazioni per questa selezione" "Persone" diff --git a/features/home/impl/src/main/res/values-ko/translations.xml b/features/home/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..de823b9257 --- /dev/null +++ b/features/home/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,53 @@ + + + "이 앱의 배터리 최적화를 비활성화하여 모든 알림이 정상적으로 수신되도록 합니다." + "최적화 비활성화" + "알림이 도착하지 않나요?" + "기존의 모든 기기를 분실한 경우 복구 키를 사용하여 암호화된 ID 및 메시지 기록을 복구할 수 있습니다." + "복구 설정" + "계정을 보호하기 위해 복구를 설정하세요" + "키 저장소 및 메시지 기록에 대한 액세스를 유지하려면 복구 키를 확인하세요." + "복구 키를 입력하세요" + "복구 키를 잊으셨나요?" + "귀하의 키 저장소가 동기화되지 않았습니다" + "중요한 전화를 놓치지 않으려면 휴대폰이 잠겨 있을 때 전체 화면 알림을 허용하도록 설정을 변경하세요." + "통화 경험을 향상시키세요" + "채팅" + "스페이스" + "정말로 %1$s 에 참가하지 않고 초대를 거절하시겠어요?" + "초대 거절" + "%1$s 와의 비공개 채팅을 정말 거부하시겠습니까?" + "채팅 거절" + "초대 없음" + "%1$s (%2$s) 당신을 초대했습니다" + "이 과정은 한 번만 진행됩니다, 기다려 주셔서 감사합니다." + "계정 설정하기" + "새로운 대화 또는 방 만들기" + "필터 지우기" + "누군가에게 메시지를 보내어 시작해 보세요." + "아직 채팅이 없습니다." + "즐겨찾기" + "채팅 설정에서 채팅을 즐겨찾기에 추가할 수 있습니다. +현재는 다른 채팅을 보려면 필터를 선택 해제해야 합니다." + "아직 즐겨찾는 채팅이 없습니다." + "초대" + "보류 중인 초대가 없습니다." + "낮은 우선순위" + "아직 낮은 우선순위 채팅이 없습니다." + "다른 채팅을 보려면 필터 선택을 해제하세요." + "이 선택 항목에 대한 채팅이 없습니다." + "사람" + "아직 DM이 없습니다." + "방" + "아직 어떤 방에도 있지 않습니다." + "읽지 않은 항목" + "축하합니다! +읽지 않은 메시지가 없습니다!" + "가입 요청이 전송되었습니다" + "채팅" + "읽음으로 표시" + "읽지 않음으로 표시" + "이 방이 업그레이드되었습니다" + "새 장치를 사용 중인 것 같습니다. 다른 디바이스로 인증하여 암호화된 메시지에 액세스하세요." + "본인인지 확인하세요" + diff --git a/features/home/impl/src/main/res/values-nb/translations.xml b/features/home/impl/src/main/res/values-nb/translations.xml index 558460a732..198bb7112d 100644 --- a/features/home/impl/src/main/res/values-nb/translations.xml +++ b/features/home/impl/src/main/res/values-nb/translations.xml @@ -13,6 +13,7 @@ "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" "Chatter" + "Områder" "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?" 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 index 0a9a608413..7aab508a38 100644 --- a/features/home/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/home/impl/src/main/res/values-pt-rBR/translations.xml @@ -3,7 +3,7 @@ "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." + "Recupere sua identidade criptográfica e o histórico de mensagens com uma chave de recuperação caso você perda 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." @@ -13,11 +13,12 @@ "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" "Conversas" - "Tem certeza de que deseja recusar o convite para ingressar em %1$s?" + "Espaços" + "Tem certeza de que deseja recusar o convite para entrar em %1$s?" "Recusar convite" - "Tem certeza de que deseja recusar esse chat privado com %1$s?" + "Tem certeza de que deseja recusar esse conversa privada com %1$s?" "Recusar chat" - "Sem convites" + "Não há convites" "%1$s(%2$s) convidou você" "Este é um processo único, obrigado por esperar." "Configurando sua conta." @@ -26,8 +27,8 @@ "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ê pode adicionar uma conversa aos seus favoritos nas configurações da conversa. +Por enquanto, você pode desmarcar os filtros para ver suas outras conversas" "Você não tem nenhuma conversa favorita ainda" "Convites" "Você não tem nenhum convite pendente." @@ -35,16 +36,16 @@ Por enquanto, você pode desmarcar os filtros para ver seus outros bate-papos""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" + "Você não tem nenhuma conversa privada ainda" "Salas" "Você não está em nenhuma sala ainda" - "Não lidos" + "Não lidas" "Parabéns! Você não tem nenhuma mensagem não lida!" - "Pedido de adesão enviado" + "Pedido de entrada enviado" "Conversas" - "Marcar como lido" - "Marcar como não lido" + "Marcar como lida" + "Marcar como não lida" "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 index 2a63000ea0..6c9f5a6fbd 100644 --- a/features/home/impl/src/main/res/values-pt/translations.xml +++ b/features/home/impl/src/main/res/values-pt/translations.xml @@ -16,7 +16,7 @@ "Espaços" "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?" + "Tens a certeza que queres rejeitar esta conversa privada com %1$s?" "Rejeitar conversa" "Sem convites" "%1$s (%2$s) convidou-te" @@ -33,6 +33,7 @@ Por enquanto, podes anular a seleção dos filtros para veres as tuas outras con "Convites" "Não tens nenhum convite pendente." "Prioridade baixa" + "Ainda não tens conversas de prioridade baixa" "Podes anular a seleção dos filtros para veres as tuas outras conversas" "Não tens nenhuma conversa selecionada" "Pessoas" diff --git a/features/home/impl/src/main/res/values-ro/translations.xml b/features/home/impl/src/main/res/values-ro/translations.xml index 1be4ab7b66..36a32bf089 100644 --- a/features/home/impl/src/main/res/values-ro/translations.xml +++ b/features/home/impl/src/main/res/values-ro/translations.xml @@ -1,13 +1,19 @@ - "Recuperați-vă identitatea criptografică și istoricul mesajelor cu o cheie de recuperare dacă ați pierdut toate dispozitivele existente." + "Dezactivați optimizarea bateriei pentru această aplicație, pentru a vă asigura că toate notificările sunt primite." + "Dezactivați optimizarea" + "Nu primiți notificări?" + "Recuperați-vă identitatea criptografică și mesajele anterioare cu o cheie de recuperare dacă ați pierdut toate dispozitivele existente." "Configurați recuperarea" "Configurați recuperarea pentru a vă proteja contul" - "Backup-ul pentru chat nu este sincronizat în prezent. Trebuie să confirmați cheia de recuperare pentru a menține accesul la backup." + "Backup-ul pentru chat nu este sincronizat. Trebuie să confirmați cheia de recuperare pentru a menține accesul la backup." + "Introduceți cheia de recuperare" + "Ați uitat cheia de recuperare?" "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" "Toate conversatiile" + "Spații" "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?" @@ -17,6 +23,7 @@ "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ă" + "Ștergeți filtrele" "Începeți prin a trimite mesaje cuiva." "Nu există încă discuții." "Favorite" @@ -26,6 +33,7 @@ Deocamdată, puteți deselecta filtrele pentru a vedea celelalte chat-uri""Invitații" "Nu aveți invitații în așteptare." "Prioritate scăzută" + "Nu aveți încă niciun chat cu prioritate scăzută." "Puteți deselecta filtrele pentru a vedea celelalte chat-uri" "Nu aveți chat-uri pentru această selecție" "Persoane" @@ -39,6 +47,7 @@ Nu aveți mesaje necitite!" "Toate conversatiile" "Marcați ca citită" "Marcați ca necitită" + "Această cameră a fost modernizată." "Se pare că folosiți un dispozitiv nou. Verificați-vă identitatea cu un alt dispozitiv pentru a accesa mesajele dumneavoastră criptate." "Verificați că sunteți dumneavoastră" diff --git a/features/home/impl/src/main/res/values-ru/translations.xml b/features/home/impl/src/main/res/values-ru/translations.xml index f147166e80..5ff6a3c714 100644 --- a/features/home/impl/src/main/res/values-ru/translations.xml +++ b/features/home/impl/src/main/res/values-ru/translations.xml @@ -1,5 +1,8 @@ + "Выключите оптимизацию расхода батареи, чтобы убедиться, что все уведомления будут поступать." + "Выключить оптимизацию" + "Уведомления не поступают?" "Создайте новый ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам." "Настроить восстановление" "Для защиты вашего аккаунта рекомендуется настроить восстановление" @@ -19,6 +22,7 @@ "Это одноразовый процесс, спасибо, что подождали." "Настройка учетной записи." "Создайте новую беседу или комнату" + "Очистить фильтры" "Начните переписку с отправки сообщения." "Пока нет доступных чатов." "Избранное" diff --git a/features/home/impl/src/main/res/values-sk/translations.xml b/features/home/impl/src/main/res/values-sk/translations.xml index 62df718bc2..f44e294432 100644 --- a/features/home/impl/src/main/res/values-sk/translations.xml +++ b/features/home/impl/src/main/res/values-sk/translations.xml @@ -33,6 +33,7 @@ Zatiaľ môžete zrušiť výber filtrov, aby ste videli ostatné konverzácie"< "Pozvánky" "Nemáte žiadne čakajúce pozvánky." "Nízka priorita" + "Zatiaľ nemáte žiadne konverzácie s nízkou prioritou." "Môžete zrušiť výber filtrov, aby ste videli svoje ostatné konverzácie" "Nemáte konverzácie pre tento výber" "Ľudia" diff --git a/features/home/impl/src/main/res/values-sv/translations.xml b/features/home/impl/src/main/res/values-sv/translations.xml index ff2d495b3a..d92b351118 100644 --- a/features/home/impl/src/main/res/values-sv/translations.xml +++ b/features/home/impl/src/main/res/values-sv/translations.xml @@ -13,6 +13,7 @@ "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" "Alla chattar" + "Utrymmen" "Ä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?" @@ -32,6 +33,7 @@ För tillfället kan du avmarkera filter för att se dina andra chattar""Inbjudningar" "Du har inga väntande inbjudningar." "Låg prioritet" + "Du har inga lågprioriterade chattar ännu" "Du kan avmarkera filter för att se dina andra chattar" "Du har inga chattar för det här valet" "Personer" diff --git a/features/home/impl/src/main/res/values-uz/translations.xml b/features/home/impl/src/main/res/values-uz/translations.xml index c28d48da80..fbfeca156a 100644 --- a/features/home/impl/src/main/res/values-uz/translations.xml +++ b/features/home/impl/src/main/res/values-uz/translations.xml @@ -3,8 +3,15 @@ "Ushbu ilova uchun quvvatni optimallashtirishni oʻchirib qoʻying, barcha xabarnomalar qabul qilinganligiga ishonch hosil qilish uchun." "Optimallashtirishni o\'chiring" "Bildirishnoma kelmayaptimi?" + "Mavjud barcha qurilmalarni yoʻqotgan boʻlsangiz, kriptografik kimligingizni va xabarlar tarixini qayta tiklovchi kalit bilan saqlab qoʻying." "Qayta tiklashni sozlang" "Hisobingizni himoya qilish uchun tiklashni sozlang" + "Kalit saqlash joyingiz va xabarlar tarixingizga kirishni saqlab qolish uchun tiklash kalitingizni tasdiqlang." + "Qayta tiklash kalitingizni kiriting" + "Tiklash kalitini unutdingizmi?" + "Kalit saqlash joyi sinxronlashmagan" + "Muhim qoʻngʻiroqlarni oʻtkazib yubormasligingiz uchun telefoningiz qulflangan holatida toʻliq ekranli bildirishnomalarni ko‘rsatishga ruxsat beradigan qilib sozlamalaringizni oʻzgartiring." + "Qoʻngʻiroq tajribangizni yaxshilang" "Suhbatlar" "Haqiqatan ham qo\'shilish taklifini rad qilmoqchimisiz%1$s ?" "Taklifni rad etish" @@ -17,9 +24,26 @@ "Yangi suhbat yoki xona yarating" "Kimgadir xabar yuborishdan boshlang." "Hozircha chatlar yo‘q." + "Sevimlilar" + "Siz chat sozlamalarida suhbatni sevimlilar ro‘yxatiga qo‘shishingiz mumkin. +Hozircha, boshqa suhbatlaringizni ko‘rish uchun filtrlarni bekor qilishingiz mumkin." + "Sizda hali sevimli chatlar yo‘q" "Takliflar" + "Sizda hech qanday kutilayotgan takliflar yoʻq." + "Past darajali" + "Boshqa suhbatlaringizni koʻrish uchun filtrlarni bekor qilishingiz mumkin" + "Sizda bu tanlov uchun chatlar yo‘q" "Odamlar" + "Sizda hali hech qanday shaxsiy xabarlar yo‘q" + "Xonalar" + "Hali hech qaysi xonada emassiz" + "Oʻqilmaganlar" + "Tabriklaymiz! +Sizda oʻqilmagan xabarlar yoʻq!" + "Qo‘shilish so‘rovi yuborildi" "Suhbatlar" + "Oʻqilgan deb belgilash" + "Oʻqilmagan deb belgilash" "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 index a9f1c932a0..625ce3ceb0 100644 --- a/features/home/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/home/impl/src/main/res/values-zh-rTW/translations.xml @@ -33,6 +33,7 @@ "邀請" "您沒有任何擱置中的邀請。" "低優先度" + "您尚無任何低優先程度聊天" "您可以取消選取篩選條件以檢視其他聊天" "您並無此選擇的聊天" "夥伴" diff --git a/features/home/impl/src/main/res/values-zh/translations.xml b/features/home/impl/src/main/res/values-zh/translations.xml index 7c74d11302..805b56de5c 100644 --- a/features/home/impl/src/main/res/values-zh/translations.xml +++ b/features/home/impl/src/main/res/values-zh/translations.xml @@ -13,6 +13,7 @@ "为确保您不会错过重要来电,请更改设置以允许锁屏时的全屏通知。" "提升通话体验" "全部聊天" + "空间" "您确定要拒绝加入 %1$s 的邀请吗?" "拒绝邀请" "您确定要拒绝与 %1$s 开始私聊吗?" @@ -22,6 +23,7 @@ "这是一个一次性的过程,感谢您的等待。" "设置您的账户。" "创建新的对话或聊天室" + "清除筛选条件" "通过向某人发送消息来开始。" "还没有聊天。" "收藏夹" @@ -31,6 +33,7 @@ "邀请" "没有待处理的邀请。" "低优先级" + "您还没有任何低优先级聊天" "您可以取消选择过滤器以查看其他对话" "您没有关于此选项的聊天" "用户" @@ -44,6 +47,7 @@ "全部聊天" "标记为已读" "标记为未读" + "此房间已升级" "您似乎正在使用新设备。使用另一台设备进行验证以访问您的加密消息。" "验证是你本人" diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilderTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilderTest.kt new file mode 100644 index 0000000000..a03c0d0065 --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilderTest.kt @@ -0,0 +1,222 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.google.common.truth.Truth.assertThat +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.A_USER_ID_3 +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.sessionstorage.api.SessionData +import io.element.android.libraries.sessionstorage.test.aSessionData +import org.junit.Test + +class CurrentUserWithNeighborsBuilderTest { + @Test + fun `build on empty list returns current user`() { + val sut = CurrentUserWithNeighborsBuilder() + val matrixUser = aMatrixUser() + val list = listOf() + val result = sut.build(matrixUser, list) + assertThat(result).containsExactly(matrixUser) + } + + @Test + fun `ensure that account are sorted by position`() { + val sut = CurrentUserWithNeighborsBuilder() + val matrixUser = aMatrixUser(id = A_USER_ID.value) + val list = listOf( + aSessionData( + sessionId = A_USER_ID.value, + position = 3, + ), + aSessionData( + sessionId = A_USER_ID_2.value, + position = 2, + ), + aSessionData( + sessionId = A_USER_ID_3.value, + position = 1, + ), + ) + val result = sut.build(matrixUser, list) + assertThat(result.map { it.userId }).containsExactly( + A_USER_ID_3, + A_USER_ID_2, + A_USER_ID, + ) + } + + @Test + fun `if current user is not found, return a singleton with current user`() { + val sut = CurrentUserWithNeighborsBuilder() + val matrixUser = aMatrixUser(id = A_USER_ID.value) + val list = listOf( + aSessionData( + sessionId = A_USER_ID_2.value, + ), + aSessionData( + sessionId = A_USER_ID_3.value, + ), + ) + val result = sut.build(matrixUser, list) + assertThat(result.map { it.userId }).containsExactly( + A_USER_ID, + ) + } + + @Test + fun `one account, will return a singleton`() { + val sut = CurrentUserWithNeighborsBuilder() + val matrixUser = aMatrixUser(id = A_USER_ID.value) + val list = listOf( + aSessionData( + sessionId = A_USER_ID.value, + ), + ) + val result = sut.build(matrixUser, list) + assertThat(result.map { it.userId }).containsExactly( + A_USER_ID, + ) + } + + @Test + fun `two accounts, first is current, will return 3 items`() { + val sut = CurrentUserWithNeighborsBuilder() + val matrixUser = aMatrixUser(id = A_USER_ID.value) + val list = listOf( + aSessionData( + sessionId = A_USER_ID.value, + ), + aSessionData( + sessionId = A_USER_ID_2.value, + ), + ) + val result = sut.build(matrixUser, list) + assertThat(result.map { it.userId }).containsExactly( + A_USER_ID_2, + A_USER_ID, + A_USER_ID_2, + ) + } + + @Test + fun `two accounts, second is current, will return 3 items`() { + val sut = CurrentUserWithNeighborsBuilder() + val matrixUser = aMatrixUser(id = A_USER_ID_2.value) + val list = listOf( + aSessionData( + sessionId = A_USER_ID.value, + ), + aSessionData( + sessionId = A_USER_ID_2.value, + ), + ) + val result = sut.build(matrixUser, list) + assertThat(result.map { it.userId }).containsExactly( + A_USER_ID, + A_USER_ID_2, + A_USER_ID, + ) + } + + @Test + fun `three accounts, first is current, will return last current and next`() { + val sut = CurrentUserWithNeighborsBuilder() + val matrixUser = aMatrixUser(id = A_USER_ID.value) + val list = listOf( + aSessionData( + sessionId = A_USER_ID.value, + ), + aSessionData( + sessionId = A_USER_ID_2.value, + ), + aSessionData( + sessionId = A_USER_ID_3.value, + ), + ) + val result = sut.build(matrixUser, list) + assertThat(result.map { it.userId }).containsExactly( + A_USER_ID_3, + A_USER_ID, + A_USER_ID_2, + ) + } + + @Test + fun `three accounts, second is current, will return first current and last`() { + val sut = CurrentUserWithNeighborsBuilder() + val matrixUser = aMatrixUser(id = A_USER_ID_2.value) + val list = listOf( + aSessionData( + sessionId = A_USER_ID.value, + ), + aSessionData( + sessionId = A_USER_ID_2.value, + ), + aSessionData( + sessionId = A_USER_ID_3.value, + ), + ) + val result = sut.build(matrixUser, list) + assertThat(result.map { it.userId }).containsExactly( + A_USER_ID, + A_USER_ID_2, + A_USER_ID_3, + ) + } + + @Test + fun `three accounts, current is last, will return middle, current and first`() { + val sut = CurrentUserWithNeighborsBuilder() + val matrixUser = aMatrixUser(id = A_USER_ID_3.value) + val list = listOf( + aSessionData( + sessionId = A_USER_ID_2.value, + ), + aSessionData( + sessionId = A_USER_ID_3.value, + ), + aSessionData( + sessionId = A_USER_ID.value, + ), + ) + val result = sut.build(matrixUser, list) + assertThat(result.map { it.userId }).containsExactly( + A_USER_ID, + A_USER_ID_2, + A_USER_ID_3, + ) + } + + @Test + fun `one account, will return data from matrix user and not from db`() { + val sut = CurrentUserWithNeighborsBuilder() + val matrixUser = aMatrixUser( + id = A_USER_ID.value, + displayName = "Bob", + avatarUrl = "avatarUrl", + ) + val list = listOf( + aSessionData( + sessionId = A_USER_ID.value, + userDisplayName = "Outdated Bob", + userAvatarUrl = "outdatedAvatarUrl", + ), + ) + val result = sut.build(matrixUser, list) + assertThat(result).containsExactly( + MatrixUser( + userId = A_USER_ID, + displayName = "Bob", + avatarUrl = "avatarUrl", + ) + ) + } +} diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt new file mode 100644 index 0000000000..7d0c95befd --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.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.home.impl + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.features.home.api.HomeEntryPoint +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DefaultHomeEntryPointTest { + @Test + fun `test node builder`() { + val entryPoint = DefaultHomeEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + HomeFlowNode( + buildContext = buildContext, + plugins = plugins, + matrixClient = FakeMatrixClient(), + presenter = createHomePresenter(), + inviteFriendsUseCase = { lambdaError() }, + analyticsService = FakeAnalyticsService(), + acceptDeclineInviteView = { _, _, _, _ -> lambdaError() }, + directLogoutView = { _ -> lambdaError() }, + reportRoomEntryPoint = { _, _, _ -> lambdaError() }, + declineInviteAndBlockUserEntryPoint = { _, _, _ -> lambdaError() }, + changeRoomMemberRolesEntryPoint = { _, _ -> lambdaError() }, + leaveRoomRenderer = { _, _, _ -> lambdaError() }, + ) + } + val callback = object : HomeEntryPoint.Callback { + override fun onRoomClick(roomId: RoomId) = lambdaError() + override fun onStartChatClick() = lambdaError() + override fun onSettingsClick() = lambdaError() + override fun onSetUpRecoveryClick() = lambdaError() + override fun onSessionConfirmRecoveryKeyClick() = lambdaError() + override fun onRoomSettingsClick(roomId: RoomId) = lambdaError() + override fun onReportBugClick() = lambdaError() + } + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .callback(callback) + .build() + assertThat(result).isInstanceOf(HomeFlowNode::class.java) + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt index a7917af658..8e5b35de99 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt @@ -12,8 +12,11 @@ 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.home.impl.spaces.HomeSpacesState +import io.element.android.features.home.impl.spaces.aHomeSpacesState import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.rageshake.api.RageshakeFeatureAvailability +import io.element.android.libraries.architecture.Presenter 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 @@ -29,10 +32,13 @@ 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.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.aSessionData +import io.element.android.tests.testutils.MutablePresenter import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -51,19 +57,32 @@ class HomePresenterTest { val presenter = createHomePresenter( client = matrixClient, rageshakeFeatureAvailability = { flowOf(false) }, + sessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData( + sessionId = matrixClient.sessionId.value, + userDisplayName = null, + userAvatarUrl = null, + ) + ), + ), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.matrixUser).isEqualTo(MatrixUser(A_USER_ID)) + assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo( + MatrixUser(A_USER_ID, null, null) + ) assertThat(initialState.canReportBug).isFalse() + skipItems(1) 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.currentUserAndNeighbors.first()).isEqualTo( + MatrixUser(A_USER_ID, A_USER_NAME, AN_AVATAR_URL) + ) assertThat(withUserState.showAvatarIndicator).isFalse() assertThat(withUserState.isSpaceFeatureEnabled).isFalse() + assertThat(withUserState.showNavigationBar).isFalse() } } @@ -71,6 +90,9 @@ class HomePresenterTest { fun `present - can report bug`() = runTest { val presenter = createHomePresenter( rageshakeFeatureAvailability = { flowOf(true) }, + sessionStore = InMemorySessionStore( + updateUserProfileResult = { _, _, _ -> }, + ), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -88,6 +110,9 @@ class HomePresenterTest { featureFlagService = FakeFeatureFlagService( initialState = mapOf(FeatureFlags.Space.key to true), ), + sessionStore = InMemorySessionStore( + updateUserProfileResult = { _, _, _ -> }, + ), ) presenter.test { skipItems(1) @@ -101,6 +126,9 @@ class HomePresenterTest { val indicatorService = FakeIndicatorService() val presenter = createHomePresenter( indicatorService = indicatorService, + sessionStore = InMemorySessionStore( + updateUserProfileResult = { _, _, _ -> }, + ), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -120,19 +148,28 @@ class HomePresenterTest { userAvatarUrl = null, ) matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.failure(AN_EXCEPTION)) - val presenter = createHomePresenter(client = matrixClient) + val presenter = createHomePresenter( + client = matrixClient, + sessionStore = InMemorySessionStore( + updateUserProfileResult = { _, _, _ -> }, + ), + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.matrixUser).isEqualTo(MatrixUser(matrixClient.sessionId)) + assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(MatrixUser(matrixClient.sessionId)) // No new state is coming } } @Test fun `present - NavigationBar change`() = runTest { - val presenter = createHomePresenter() + val presenter = createHomePresenter( + sessionStore = InMemorySessionStore( + updateUserProfileResult = { _, _, _ -> }, + ), + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -144,21 +181,56 @@ class HomePresenterTest { } } - private fun TestScope.createHomePresenter( - client: MatrixClient = FakeMatrixClient(), - syncService: SyncService = FakeSyncService(), - snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), - rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(false) }, - indicatorService: IndicatorService = FakeIndicatorService(), - featureFlagService: FeatureFlagService = FakeFeatureFlagService() - ) = HomePresenter( - client = client, - syncService = syncService, - snackbarDispatcher = snackbarDispatcher, - indicatorService = indicatorService, - logoutPresenter = { aDirectLogoutState() }, - roomListPresenter = { aRoomListState() }, - rageshakeFeatureAvailability = rageshakeFeatureAvailability, - featureFlagService = featureFlagService, - ) + @Test + fun `present - NavigationBar is hidden when the last space is left`() = runTest { + val homeSpacesPresenter = MutablePresenter(aHomeSpacesState()) + val presenter = createHomePresenter( + sessionStore = InMemorySessionStore( + updateUserProfileResult = { _, _, _ -> }, + ), + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.Space.key to true), + ), + homeSpacesPresenter = homeSpacesPresenter, + ) + presenter.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats) + assertThat(initialState.showNavigationBar).isTrue() + // User navigate to Spaces + initialState.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces)) + val spaceState = awaitItem() + assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces) + // The last space is left + homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList())) + skipItems(1) + val finalState = awaitItem() + // We are back to Chats + assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats) + assertThat(finalState.showNavigationBar).isFalse() + } + } } + +internal fun createHomePresenter( + client: MatrixClient = FakeMatrixClient(), + syncService: SyncService = FakeSyncService(), + snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), + rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(false) }, + indicatorService: IndicatorService = FakeIndicatorService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), + homeSpacesPresenter: Presenter = Presenter { aHomeSpacesState() }, + sessionStore: SessionStore = InMemorySessionStore(), +) = HomePresenter( + client = client, + syncService = syncService, + snackbarDispatcher = snackbarDispatcher, + indicatorService = indicatorService, + logoutPresenter = { aDirectLogoutState() }, + roomListPresenter = { aRoomListState() }, + homeSpacesPresenter = homeSpacesPresenter, + rageshakeFeatureAvailability = rageshakeFeatureAvailability, + featureFlagService = featureFlagService, + sessionStore = sessionStore, +) diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt index 044c150ad2..7ad58f2832 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt @@ -24,6 +24,7 @@ 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.api.acceptdecline.anAcceptDeclineInviteState import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenterTest.kt new file mode 100644 index 0000000000..ef75dcad81 --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenterTest.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.home.impl.spaces + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.features.invite.test.InMemorySeenInvitesStore +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class HomeSpacesPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + presenter.test { + val state = awaitItem() + assertThat(state.space).isEqualTo(CurrentSpace.Root) + assertThat(state.spaceRooms).isEmpty() + assertThat(state.hideInvitesAvatar).isFalse() + assertThat(state.seenSpaceInvites).isEmpty() + } + } + + private fun createPresenter( + client: MatrixClient = FakeMatrixClient(), + seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(), + ) = HomeSpacesPresenter( + client = client, + seenInvitesStore = seenInvitesStore, + ) +} diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt index eb1dfd3df6..fa296edc1c 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt @@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo +import io.element.android.libraries.matrix.api.spaces.SpaceRoom import kotlinx.parcelize.Parcelize @Parcelize @@ -36,3 +37,11 @@ fun RoomInfo.toInviteData(): InviteData { isDm = isDm, ) } + +fun SpaceRoom.toInviteData(): InviteData { + return InviteData( + roomId = roomId, + roomName = name ?: roomId.value, + isDm = false, + ) +} diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt new file mode 100644 index 0000000000..bd5c5e6749 --- /dev/null +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.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.features.invite.api.acceptdecline + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId + +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/acceptdecline/AcceptDeclineInviteView.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteView.kt index 78be5ebd16..a098443c45 100644 --- 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 @@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import io.element.android.libraries.matrix.api.core.RoomId -interface AcceptDeclineInviteView { +fun interface AcceptDeclineInviteView { @Composable fun Render( state: AcceptDeclineInviteState, 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 index 27080ee354..8517fe2786 100644 --- 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 @@ -12,6 +12,6 @@ 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 interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint { fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node } diff --git a/features/invite/impl/build.gradle.kts b/features/invite/impl/build.gradle.kts index d6337f65c9..4caeafb71f 100644 --- a/features/invite/impl/build.gradle.kts +++ b/features/invite/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.invite.api) @@ -36,17 +37,9 @@ dependencies { implementation(projects.services.analytics.api) implementation(projects.libraries.push.api) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs, true) 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 index 716cd7f907..0972701435 100644 --- 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 @@ -7,7 +7,8 @@ package io.element.android.features.invite.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.libraries.core.extensions.mapFailure @@ -19,7 +20,6 @@ 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 @@ -30,7 +30,8 @@ interface AcceptInvite { } @ContributesBinding(SessionScope::class) -class DefaultAcceptInvite @Inject constructor( +@Inject +class DefaultAcceptInvite( private val client: MatrixClient, private val joinRoom: JoinRoom, private val notificationCleaner: NotificationCleaner, 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 index 9276f37365..6c2588de7e 100644 --- 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 @@ -7,13 +7,13 @@ package io.element.android.features.invite.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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( @@ -32,7 +32,8 @@ interface DeclineInvite { } @ContributesBinding(SessionScope::class) -class DefaultDeclineInvite @Inject constructor( +@Inject +class DefaultDeclineInvite( private val client: MatrixClient, private val notificationCleaner: NotificationCleaner, private val seenInvitesStore: SeenInvitesStore, 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 index 256214e5d2..10812eeb80 100644 --- 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 @@ -8,20 +8,21 @@ package io.element.android.features.invite.impl import android.content.Context -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.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.di.annotations.ApplicationContext 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( +@Inject +class DefaultSeenInvitesStoreFactory( @ApplicationContext private val context: Context, private val sessionObserver: SessionObserver, ) : SeenInvitesStoreFactory { 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 index 163912392e..7d15971fe4 100644 --- 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 @@ -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 dev.zacsweers.metro.Inject 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 @@ -24,9 +25,9 @@ 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( +@Inject +class AcceptDeclineInvitePresenter( private val acceptInvite: AcceptInvite, private val declineInvite: DeclineInvite, ) : Presenter { diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt index 9896de1d3e..6db000d3db 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt @@ -9,9 +9,9 @@ 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.api.acceptdecline.anAcceptDeclineInviteState import io.element.android.features.invite.impl.AcceptInvite import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId @@ -51,13 +51,3 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider = 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/DefaultAcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/DefaultAcceptDeclineInviteView.kt index e37582f63f..487865065d 100644 --- 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 @@ -9,15 +9,16 @@ 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 dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 { +@Inject +class DefaultAcceptDeclineInviteView : AcceptDeclineInviteView { @Composable override fun Render( state: AcceptDeclineInviteState, 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 index 100a5de4b8..51cdf59b6a 100644 --- 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 @@ -12,16 +12,17 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.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( +@AssistedInject +class DeclineAndBlockNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: DeclineAndBlockPresenter.Factory, 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 index 9a18aa746b..59812a5e04 100644 --- 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 @@ -15,9 +15,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.invite.api.InviteData import io.element.android.features.invite.impl.DeclineInvite import io.element.android.libraries.architecture.AsyncAction @@ -28,13 +28,14 @@ import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class DeclineAndBlockPresenter @AssistedInject constructor( +@AssistedInject +class DeclineAndBlockPresenter( @Assisted private val inviteData: InviteData, private val declineInvite: DeclineInvite, private val snackbarDispatcher: SnackbarDispatcher, ) : Presenter { @AssistedFactory - interface Factory { + fun interface Factory { fun create(inviteData: InviteData): DeclineAndBlockPresenter } 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 index 5eab22dd91..ea5456feb2 100644 --- 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 @@ -9,15 +9,16 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 { +@Inject +class DefaultDeclineAndBlockEntryPoint : 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 4f5da603a6..3f62408b20 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 @@ -7,10 +7,10 @@ package io.element.android.features.invite.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.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 @@ -20,7 +20,7 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient @ContributesTo(SessionScope::class) -@Module +@BindingContainer interface InviteModule { @Binds fun bindAcceptDeclinePresenter(presenter: AcceptDeclineInvitePresenter): Presenter 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 5e3c9a6fbd..580bc68ffd 100644 --- a/features/invite/impl/src/main/res/values-bg/translations.xml +++ b/features/invite/impl/src/main/res/values-bg/translations.xml @@ -3,6 +3,8 @@ "Блокиране на потребителя" "Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?" "Отказване на покана" + "Сигурни ли сте, че искате да откажете този личен чат с %1$s?" + "Отказване на чат" "Няма покани" "%1$s (%2$s) ви покани" 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 202000d45f..bb118a99e8 100644 --- a/features/invite/impl/src/main/res/values-de/translations.xml +++ b/features/invite/impl/src/main/res/values-de/translations.xml @@ -1,18 +1,18 @@ - "Sie werden keine Nachrichten oder Chateinladungen von diesem Nutzer sehen." + "Du wirst keine Nachrichten oder Chat-Einladungen von diesem Nutzer sehen." "Nutzer blockieren" - "Melden Sie diesen Raum Ihrem Kontoanbieter." - "Nennen Sie den Grund für die Meldung…" + "Melde diesen Chat deinem Konto-Anbieter." + "Nenne den Grund für die Meldung…" "Ablehnen und blockieren" - "Möchten Sie die Einladung zum Betreten von %1$s wirklich ablehnen?" + "Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?" "Einladung ablehnen" - "Möchten Sie diesen privaten Chat mit %1$s wirklich 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" "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." + "Bist du sicher, dass du die Einladung zu diesem Chat ablehnen möchtest? Dadurch wird auch jede weitere Kontaktaufnahme oder Chat Einladung von %1$s blockiert." "Einladung ablehnen & Nutzer blockieren" "Ablehnen und blockieren" 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 97595ed421..93072bc31a 100644 --- a/features/invite/impl/src/main/res/values-hu/translations.xml +++ b/features/invite/impl/src/main/res/values-hu/translations.xml @@ -4,7 +4,7 @@ "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" + "Elutasítás és letiltá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?" diff --git a/features/invite/impl/src/main/res/values-ko/translations.xml b/features/invite/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..169d5ab668 --- /dev/null +++ b/features/invite/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,18 @@ + + + "이 사용자로부터 메시지나 방 초대장이 표시되지 않습니다." + "사용자 차단하기" + "이 room 계정 제공자에게 신고하세요." + "신고 사유를 설명하세요…" + "거부 및 차단" + "정말로 %1$s 에 참가하지 않고 초대를 거절하시겠어요?" + "초대 거절" + "%1$s 와의 비공개 채팅을 정말 거부하시겠습니까?" + "채팅 거절" + "초대 없음" + "%1$s (%2$s) 당신을 초대했습니다" + "예, 거부 및 차단" + "이 방에 대한 초대 거부를 정말로 확인하시겠습니까? 이 경우 %1$s 에서 귀하에게 연락하거나 방에 초대할 수 없게 됩니다." + "초대 거부 및 차단" + "거부 및 차단" + 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 43569cdd31..f82f98cb1c 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 @@ -5,14 +5,14 @@ "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?" + "Tem certeza de que deseja recusar o convite para entrar em %1$s?" "Recusar convite" - "Tem certeza de que deseja recusar esse chat privado com %1$s?" + "Tem certeza de que deseja recusar esse conversa privada com %1$s?" "Recusar chat" - "Sem convites" + "Não há 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." + "Tem certeza de que quer recusar o convite para entrar nesta sala? Isso também impedirá que %1$s entre em contato com você ou o convide para 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 5813c9e54f..513f14eea6 100644 --- a/features/invite/impl/src/main/res/values-pt/translations.xml +++ b/features/invite/impl/src/main/res/values-pt/translations.xml @@ -1,13 +1,13 @@ - "Não verás quaisquer mensagens ou convites deste utilizador" + "Não vais ver quaisquer mensagens ou convites para sala deste utilizador" "Bloquear utilizador" - "Denunciar esta sala ao teu operador de conta." - "Descreve a razão de denúncia…" + "Denunciar esta sala ao fornecedor da tua conta." + "Descreve a razão para bloquear…" "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?" + "Tens a certeza que queres rejeitar esta conversa privada com %1$s?" "Rejeitar conversa" "Sem convites" "%1$s (%2$s) convidou-te" diff --git a/features/invite/impl/src/main/res/values-ro/translations.xml b/features/invite/impl/src/main/res/values-ro/translations.xml index 1856b36021..747efbed6d 100644 --- a/features/invite/impl/src/main/res/values-ro/translations.xml +++ b/features/invite/impl/src/main/res/values-ro/translations.xml @@ -1,10 +1,18 @@ + "Nu veți vedea niciun mesaj sau invitație de la acest utilizator." "Blocați utilizatorul" + "Raportați această cameră furnizorului contului dumneavoastră" + "Descrieți motivul raportării…" + "Refuzați și blocați" "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." + "Da, refuzați și blocați" + "Sunteți sigur că doriți să refuzați invitația de a vă alătura acestei camere? Acest lucru va împiedica, de asemenea, %1$s să vă contacteze sau să vă invite în camere." + "Refuzați invitația și blocați" + "Refuzați și blocați" 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 index c5d2f53c92..651b7bb6bb 100644 --- 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 @@ -11,11 +11,11 @@ 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.features.invite.test.anInviteData 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 @@ -148,28 +148,16 @@ class DeclineAndBlockPresenterTest { .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, - ) - } +} + +internal 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/DefaultDeclineAndBlockEntryPointTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPointTest.kt new file mode 100644 index 0000000000..7cdf208b9a --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPointTest.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.features.invite.impl.declineandblock + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.features.invite.test.anInviteData +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultDeclineAndBlockEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultDeclineAndBlockEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + DeclineAndBlockNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { inviteData -> createDeclineAndBlockPresenter() } + ) + } + val inviteData = anInviteData() + val result = entryPoint.createNode( + parentNode = parentNode, + buildContext = BuildContext.root(null), + inviteData = inviteData + ) + assertThat(result).isInstanceOf(DeclineAndBlockNode::class.java) + assertThat(result.plugins).contains(DeclineAndBlockNode.Inputs(inviteData)) + } +} diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt index db8b9ffbd2..75fd211295 100644 --- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt +++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt @@ -7,8 +7,11 @@ package io.element.android.features.invitepeople.api +import io.element.android.libraries.architecture.AsyncAction + interface InvitePeopleState { val canInvite: Boolean val isSearchActive: Boolean + val sendInvitesAction: AsyncAction val eventSink: (InvitePeopleEvents) -> Unit } diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt index b69ad7c225..fdcebb17a2 100644 --- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt +++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt @@ -8,28 +8,33 @@ package io.element.android.features.invitepeople.api import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction class InvitePeopleStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aPreviewInvitePeopleState(), aPreviewInvitePeopleState(canInvite = true), - aPreviewInvitePeopleState(isSearchActive = true) + aPreviewInvitePeopleState(isSearchActive = true), + aPreviewInvitePeopleState(sendInvitesAction = AsyncAction.Loading), ) } private data class PreviewInvitePeopleState( override val canInvite: Boolean, override val isSearchActive: Boolean, + override val sendInvitesAction: AsyncAction, override val eventSink: (InvitePeopleEvents) -> Unit, ) : InvitePeopleState private fun aPreviewInvitePeopleState( canInvite: Boolean = false, isSearchActive: Boolean = false, + sendInvitesAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (InvitePeopleEvents) -> Unit = {}, ) = PreviewInvitePeopleState( canInvite = canInvite, isSearchActive = isSearchActive, + sendInvitesAction = sendInvitesAction, eventSink = eventSink ) diff --git a/features/invitepeople/impl/build.gradle.kts b/features/invitepeople/impl/build.gradle.kts index bdb1c6942e..f0fcde6a00 100644 --- a/features/invitepeople/impl/build.gradle.kts +++ b/features/invitepeople/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -22,7 +23,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.core) @@ -37,17 +38,8 @@ dependencies { implementation(projects.services.apperror.api) api(projects.features.invitepeople.api) - testImplementation(libs.test.junit) - testImplementation(libs.test.mockk) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.usersearch.test) testImplementation(projects.services.apperror.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt index 8961e1157f..5e2b00a3f1 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt @@ -16,16 +16,18 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import com.squareup.anvil.annotations.ContributesBinding -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesBinding import io.element.android.features.invitepeople.api.InvitePeopleEvents import io.element.android.features.invitepeople.api.InvitePeoplePresenter import io.element.android.features.invitepeople.api.InvitePeopleState +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.map import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.di.SessionScope @@ -49,7 +51,8 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class DefaultInvitePeoplePresenter @AssistedInject constructor( +@AssistedInject +class DefaultInvitePeoplePresenter( @Assisted private val joinedRoom: JoinedRoom?, @Assisted private val roomId: RoomId, private val userRepository: UserRepository, @@ -72,6 +75,8 @@ class DefaultInvitePeoplePresenter @AssistedInject constructor( var searchQuery by rememberSaveable { mutableStateOf("") } var searchActive by rememberSaveable { mutableStateOf(false) } val showSearchLoader = rememberSaveable { mutableStateOf(false) } + val sendInvitesAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) { if (joinedRoom == null) { val result = matrixClient.getJoinedRoom(roomId) @@ -115,7 +120,7 @@ class DefaultInvitePeoplePresenter @AssistedInject constructor( } is InvitePeopleEvents.SendInvites -> { room.dataOrNull()?.let { - sessionCoroutineScope.sendInvites(it, selectedUsers.value) + sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction) } } is InvitePeopleEvents.CloseSearch -> { @@ -127,12 +132,13 @@ class DefaultInvitePeoplePresenter @AssistedInject constructor( return DefaultInvitePeopleState( room = room.map { }, - canInvite = selectedUsers.value.isNotEmpty(), + canInvite = selectedUsers.value.isNotEmpty() && !sendInvitesAction.value.isLoading(), selectedUsers = selectedUsers.value, searchQuery = searchQuery, isSearchActive = searchActive, searchResults = searchResults.value, showSearchLoader = showSearchLoader.value, + sendInvitesAction = sendInvitesAction.value, eventSink = ::handleEvents, ) } @@ -140,16 +146,21 @@ class DefaultInvitePeoplePresenter @AssistedInject constructor( private fun CoroutineScope.sendInvites( room: JoinedRoom, selectedUsers: List, + sendInvitesAction: MutableState>, ) = launch { - val anyInviteFailed = selectedUsers - .map { room.inviteUserById(it.userId) } - .any { it.isFailure } + sendInvitesAction.runUpdatingState { + val anyInviteFailed = selectedUsers + .map { room.inviteUserById(it.userId) } + .any { it.isFailure } - if (anyInviteFailed) { - appErrorStateService.showError( - titleRes = CommonStrings.common_unable_to_invite_title, - bodyRes = CommonStrings.common_unable_to_invite_message, - ) + if (anyInviteFailed) { + appErrorStateService.showError( + titleRes = CommonStrings.common_unable_to_invite_title, + bodyRes = CommonStrings.common_unable_to_invite_message, + ) + } + + Result.success(Unit) } } diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleRenderer.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleRenderer.kt index 8207e75fd5..3301b7ec2c 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleRenderer.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleRenderer.kt @@ -9,14 +9,15 @@ package io.element.android.features.invitepeople.impl import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.invitepeople.api.InvitePeopleRenderer import io.element.android.features.invitepeople.api.InvitePeopleState import io.element.android.libraries.di.SessionScope -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultInvitePeopleRenderer @Inject constructor() : InvitePeopleRenderer { +@Inject +class DefaultInvitePeopleRenderer : InvitePeopleRenderer { @Composable override fun Render(state: InvitePeopleState, modifier: Modifier) { if (state is DefaultInvitePeopleState) { diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt index 77ba8aad05..5ae51a8698 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt @@ -9,6 +9,7 @@ package io.element.android.features.invitepeople.impl import io.element.android.features.invitepeople.api.InvitePeopleEvents import io.element.android.features.invitepeople.api.InvitePeopleState +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.user.MatrixUser @@ -22,5 +23,6 @@ data class DefaultInvitePeopleState( val searchResults: SearchBarResultState>, val selectedUsers: ImmutableList, override val isSearchActive: Boolean, + override val sendInvitesAction: AsyncAction, override val eventSink: (InvitePeopleEvents) -> Unit ) : InvitePeopleState diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt index 980d32e7fb..ebcd932a55 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.invitepeople.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.user.MatrixUser @@ -68,6 +69,11 @@ internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider = persistentListOf(), isSearchActive: Boolean = false, showSearchLoader: Boolean = false, + sendInvitesAction: AsyncAction = AsyncAction.Uninitialized, ): DefaultInvitePeopleState { return DefaultInvitePeopleState( room = room, @@ -102,6 +109,7 @@ private fun aDefaultInvitePeopleState( selectedUsers = selectedUsers, isSearchActive = isSearchActive, showSearchLoader = showSearchLoader, + sendInvitesAction = sendInvitesAction, eventSink = {}, ) } diff --git a/features/invitepeople/impl/src/main/res/values-ko/translations.xml b/features/invitepeople/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..19214c5a79 --- /dev/null +++ b/features/invitepeople/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,5 @@ + + + "이미 회원" + "이미 초대됨" + diff --git a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt index 1e438fab8e..e51c3ee352 100644 --- a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt +++ b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt @@ -409,10 +409,23 @@ internal class DefaultInvitePeoplePresenterTest { assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) // Send invites initialState.eventSink(InvitePeopleEvents.SendInvites) + + // Can't invite in the loading state + awaitItem().run { + assertThat(sendInvitesAction.isLoading()).isTrue() + assertThat(canInvite).isFalse() + } + delay(1_000) inviteUserResult.assertions().isCalledOnce().with( value(selectedUser.userId) ) + + // Can invite again once the action is finished + awaitItem().run { + assertThat(sendInvitesAction.isReady()).isTrue() + assertThat(canInvite).isTrue() + } } } @@ -445,6 +458,13 @@ internal class DefaultInvitePeoplePresenterTest { assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) // Send invites initialState.eventSink(InvitePeopleEvents.SendInvites) + + // Can't invite in the loading state + awaitItem().run { + assertThat(sendInvitesAction.isLoading()).isTrue() + assertThat(canInvite).isFalse() + } + delay(1_000) inviteUserResult.assertions().isCalledOnce().with( value(selectedUser.userId) @@ -455,6 +475,12 @@ internal class DefaultInvitePeoplePresenterTest { value(CommonStrings.common_unable_to_invite_title), value(CommonStrings.common_unable_to_invite_message) ) + + // Can invite again once the action is finished + awaitItem().run { + assertThat(sendInvitesAction.isReady()).isTrue() + assertThat(canInvite).isTrue() + } } } diff --git a/features/joinroom/impl/build.gradle.kts b/features/joinroom/impl/build.gradle.kts index e195efd1bc..476aacd863 100644 --- a/features/joinroom/impl/build.gradle.kts +++ b/features/joinroom/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2024 New Vector Ltd. @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.joinroom.api) @@ -38,16 +39,9 @@ dependencies { implementation(projects.libraries.preferences.api) implementation(projects.appconfig) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs, true) 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) + testImplementation(projects.libraries.previewutils) } 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 e28b2affe8..5f217b26c2 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 @@ -9,14 +9,15 @@ package io.element.android.features.joinroom.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.joinroom.api.JoinRoomEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultJoinRoomEntryPoint @Inject constructor() : JoinRoomEntryPoint { +@Inject +class DefaultJoinRoomEntryPoint : JoinRoomEntryPoint { override fun createNode(parentNode: Node, buildContext: BuildContext, inputs: JoinRoomEntryPoint.Inputs): Node { return parentNode.createNode( buildContext = buildContext, 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 index ecd4c920c8..f501752544 100644 --- 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 @@ -16,9 +16,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.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 @@ -30,7 +30,8 @@ import io.element.android.libraries.di.SessionScope import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -class JoinRoomFlowNode @AssistedInject constructor( +@AssistedInject +class JoinRoomFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: JoinRoomPresenter.Factory, 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 6206c2426a..f7e154acf0 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 @@ -20,9 +20,10 @@ 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.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.JoinedRoom +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 @@ -34,7 +35,6 @@ 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 @@ -43,18 +43,24 @@ 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.RoomInfo -import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipDetails import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo +import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.ui.model.toInviteSender +import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.util.Optional +import kotlin.jvm.optionals.getOrNull -class JoinRoomPresenter @AssistedInject constructor( +@AssistedInject +class JoinRoomPresenter( @Assisted private val roomId: RoomId, @Assisted private val roomIdOrAlias: RoomIdOrAlias, @Assisted private val roomDescription: Optional, @@ -69,7 +75,7 @@ class JoinRoomPresenter @AssistedInject constructor( private val buildMeta: BuildMeta, private val seenInvitesStore: SeenInvitesStore, ) : Presenter { - interface Factory { + fun interface Factory { fun create( roomId: RoomId, roomIdOrAlias: RoomIdOrAlias, @@ -79,6 +85,8 @@ class JoinRoomPresenter @AssistedInject constructor( ): JoinRoomPresenter } + private val spaceList = matrixClient.spaceService.spaceRoomList(roomId) + @Composable override fun present(): JoinRoomState { val coroutineScope = rememberCoroutineScope() @@ -86,69 +94,51 @@ class JoinRoomPresenter @AssistedInject constructor( val roomInfo by remember { matrixClient.getRoomInfoFlow(roomId) }.collectAsState(initial = Optional.empty()) + val spaceRoom by spaceList.currentSpaceFlow.collectAsState() 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 hideInviteAvatars by matrixClient.rememberHideInvitesAvatar() val canReportRoom by produceState(false) { value = matrixClient.canReportRoom() } - val contentState by produceState( - initialValue = ContentState.Loading, - key1 = roomInfo, - key2 = retryCount, - key3 = isDismissingContent, - ) { + var contentState by remember { + mutableStateOf(ContentState.Loading) + } + LaunchedEffect(roomInfo, retryCount, isDismissingContent, spaceRoom) { when { - isDismissingContent -> value = ContentState.Dismissing + isDismissingContent -> contentState = ContentState.Dismissing roomInfo.isPresent -> { 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 membershipDetails = notJoinedRoom?.membershipDetails()?.getOrNull() val joinedMembersCountOverride = notJoinedRoom?.previewInfo?.numberOfJoinedMembers - value = roomInfo.get().toContentState( - membershipSender = sender, + contentState = roomInfo.get().toContentState( joinedMembersCountOverride = joinedMembersCountOverride, - reason = reason, + membershipDetails = membershipDetails, + childrenCount = spaceRoom.getOrNull()?.childrenCount, ) } + spaceRoom.isPresent -> { + val spaceRoom = spaceRoom.get() + // Only use this state when space is not locally known + contentState = if (spaceRoom.state != null) { + ContentState.Loading + } else { + spaceRoom.toContentState() + } + } roomDescription.isPresent -> { - value = roomDescription.get().toContentState() + contentState = roomDescription.get().toContentState() } else -> { - value = ContentState.Loading + contentState = ContentState.Loading val result = matrixClient.getRoomPreview(roomIdOrAlias, serverNames) - value = result.fold( + contentState = result.fold( 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, - ) + val membershipDetails = preview.membershipDetails().getOrNull() + preview.previewInfo.toContentState(membershipDetails) }, onFailure = { throwable -> if (throwable is ClientException.MatrixApi && (throwable.kind == ErrorKind.NotFound || throwable.kind == ErrorKind.Forbidden)) { @@ -256,30 +246,56 @@ class JoinRoomPresenter @AssistedInject constructor( } } -private fun RoomPreviewInfo.toContentState(senderMember: RoomMember?, reason: String?): ContentState { +private fun RoomPreviewInfo.toContentState(membershipDetails: RoomMembershipDetails?): ContentState { return ContentState.Loaded( roomId = roomId, name = name, topic = topic, alias = canonicalAlias, numberOfMembers = numberOfJoinedMembers, - isDm = false, - roomType = roomType, roomAvatarUrl = avatarUrl, - 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() + joinAuthorisationStatus = computeJoinAuthorisationStatus( + membership, + membershipDetails, + joinRule, + { toInviteData() } + ), + joinRule = joinRule, + details = when (roomType) { + is RoomType.Other, + RoomType.Room -> LoadedDetails.Room( + isDm = false, + ) + RoomType.Space -> LoadedDetails.Space( + childrenCount = 0, + heroes = persistentListOf(), + ) } ) } +private fun SpaceRoom.toContentState(): ContentState { + return ContentState.Loaded( + roomId = roomId, + name = name, + topic = topic, + alias = canonicalAlias, + numberOfMembers = numJoinedMembers.toLong(), + roomAvatarUrl = avatarUrl, + joinAuthorisationStatus = computeJoinAuthorisationStatus( + membership = state, + membershipDetails = null, + joinRule = joinRule, + inviteData = { toInviteData() } + ), + joinRule = joinRule, + details = LoadedDetails.Space( + childrenCount = childrenCount, + heroes = heroes.toPersistentList(), + ) + ) +} + @VisibleForTesting internal fun RoomDescription.toContentState(): ContentState { return ContentState.Loaded( @@ -288,22 +304,29 @@ internal fun RoomDescription.toContentState(): ContentState { topic = topic, alias = alias, numberOfMembers = numberOfMembers, - isDm = false, - roomType = RoomType.Room, roomAvatarUrl = avatarUrl, joinAuthorisationStatus = when (joinRule) { RoomDescription.JoinRule.KNOCK -> JoinAuthorisationStatus.CanKnock RoomDescription.JoinRule.PUBLIC -> JoinAuthorisationStatus.CanJoin else -> JoinAuthorisationStatus.Unknown - } + }, + joinRule = when (joinRule) { + RoomDescription.JoinRule.KNOCK -> JoinRule.Knock + RoomDescription.JoinRule.PUBLIC -> JoinRule.Public + RoomDescription.JoinRule.RESTRICTED -> JoinRule.Restricted(persistentListOf()) + RoomDescription.JoinRule.KNOCK_RESTRICTED -> JoinRule.KnockRestricted(persistentListOf()) + RoomDescription.JoinRule.INVITE -> JoinRule.Invite + RoomDescription.JoinRule.UNKNOWN -> null + }, + details = LoadedDetails.Room(isDm = false) ) } @VisibleForTesting internal fun RoomInfo.toContentState( - membershipSender: RoomMember?, joinedMembersCountOverride: Long?, - reason: String?, + membershipDetails: RoomMembershipDetails?, + childrenCount: Int?, ): ContentState { return ContentState.Loaded( roomId = id, @@ -311,24 +334,49 @@ internal fun RoomInfo.toContentState( topic = topic, alias = canonicalAlias, numberOfMembers = joinedMembersCountOverride ?: joinedMembersCount, - isDm = isDm, - roomType = if (isSpace) RoomType.Space else RoomType.Room, roomAvatarUrl = avatarUrl, - joinAuthorisationStatus = when (currentUserMembership) { - CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited( - inviteData = toInviteData(), - inviteSender = membershipSender?.toInviteSender(), + joinAuthorisationStatus = computeJoinAuthorisationStatus( + membership = currentUserMembership, + membershipDetails = membershipDetails, + joinRule = joinRule, + inviteData = { toInviteData() } + ), + joinRule = joinRule, + details = if (isSpace) { + LoadedDetails.Space( + childrenCount = childrenCount ?: 0, + heroes = heroes, ) - CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned( - banSender = membershipSender?.toInviteSender(), - reason = reason, + } else { + LoadedDetails.Room( + isDm = isDm, ) - CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked - else -> joinRule.toJoinAuthorisationStatus() - } + }, ) } +private fun computeJoinAuthorisationStatus( + membership: CurrentUserMembership?, + membershipDetails: RoomMembershipDetails?, + joinRule: JoinRule?, + inviteData: () -> InviteData, +): JoinAuthorisationStatus { + return when (membership) { + CurrentUserMembership.INVITED -> { + JoinAuthorisationStatus.IsInvited( + inviteData = inviteData(), + inviteSender = membershipDetails?.senderMember?.toInviteSender() + ) + } + CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned( + membershipDetails?.senderMember?.toInviteSender(), + membershipDetails?.membershipChangeReason + ) + CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked + else -> joinRule.toJoinAuthorisationStatus() + } +} + private fun JoinRule?.toJoinAuthorisationStatus(): JoinAuthorisationStatus { return when (this) { JoinRule.Knock, 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 5b9f8007a3..f27a290f5d 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 @@ -16,9 +16,11 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.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.api.room.join.JoinRule +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.model.InviteSender +import kotlinx.collections.immutable.ImmutableList internal const val MAX_KNOCK_MESSAGE_LENGTH = 500 @@ -41,9 +43,6 @@ data class JoinRoomState( val joinAuthorisationStatus = when (contentState) { is ContentState.Loaded -> { when { - contentState.roomType == RoomType.Space -> { - JoinAuthorisationStatus.IsSpace(applicationName) - } isJoinActionUnauthorized -> { JoinAuthorisationStatus.Unauthorized } @@ -77,12 +76,13 @@ sealed interface ContentState { val topic: String?, val alias: RoomAlias?, val numberOfMembers: Long?, - val isDm: Boolean, - val roomType: RoomType, val roomAvatarUrl: String?, val joinAuthorisationStatus: JoinAuthorisationStatus, + val joinRule: JoinRule?, + val details: LoadedDetails, ) : ContentState { val showMemberCount = numberOfMembers != null + val isSpace = details is LoadedDetails.Space fun avatarData(size: AvatarSize): AvatarData { return AvatarData( @@ -95,9 +95,20 @@ sealed interface ContentState { } } +@Immutable +sealed interface LoadedDetails { + data class Room( + val isDm: Boolean, + ) : LoadedDetails + + data class Space( + val childrenCount: Int, + val heroes: ImmutableList, + ) : LoadedDetails +} + sealed interface 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 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 8f891fe645..d0af006dd7 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 @@ -9,8 +9,8 @@ package io.element.android.features.joinroom.impl 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.anAcceptDeclineInviteState 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 @@ -20,9 +20,11 @@ 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.api.room.join.JoinRule +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.model.InviteSender +import kotlinx.collections.immutable.toPersistentList open class JoinRoomStateProvider : PreviewParameterProvider { override val values: Sequence @@ -77,13 +79,17 @@ open class JoinRoomStateProvider : PreviewParameterProvider { name = "A space", alias = null, topic = "This is the topic of a space", - roomType = RoomType.Space, + details = aLoadedDetailsSpace( + childrenCount = 42, + ), ) ), aJoinRoomState( contentState = aLoadedContentState( name = "A DM", - isDm = true, + details = aLoadedDetailsRoom( + isDm = true, + ), ) ), aJoinRoomState( @@ -156,20 +162,34 @@ fun aLoadedContentState( alias: RoomAlias? = RoomAlias("#exa:matrix.org"), topic: String? = "Element X is a secure, private and decentralized messenger.", numberOfMembers: Long? = null, - isDm: Boolean = false, - roomType: RoomType = RoomType.Room, roomAvatarUrl: String? = null, joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown, + joinRule: JoinRule? = null, + details: LoadedDetails = aLoadedDetailsRoom(isDm = false), ) = ContentState.Loaded( roomId = roomId, name = name, alias = alias, topic = topic, numberOfMembers = numberOfMembers, - isDm = isDm, - roomType = roomType, roomAvatarUrl = roomAvatarUrl, - joinAuthorisationStatus = joinAuthorisationStatus + joinAuthorisationStatus = joinAuthorisationStatus, + joinRule = joinRule, + details = details, +) + +fun aLoadedDetailsRoom( + isDm: Boolean = false, +) = LoadedDetails.Room( + isDm = isDm +) + +fun aLoadedDetailsSpace( + childrenCount: Int = 0, + heroes: List = emptyList(), +) = LoadedDetails.Space( + childrenCount = childrenCount, + heroes = heroes.toPersistentList() ) fun aJoinRoomState( @@ -199,16 +219,6 @@ fun aJoinRoomState( 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", 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 cdeac0afe9..6da7aadfe7 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 @@ -7,21 +7,20 @@ package io.element.android.features.joinroom.impl +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight 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.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -38,6 +37,7 @@ 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.invite.api.InviteData import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom @@ -46,7 +46,7 @@ import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAt import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule -import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMembersCountMolecule +import io.element.android.libraries.designsystem.atomic.molecules.MembersCountMolecule import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.components.Announcement @@ -65,14 +65,21 @@ 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.Icon +import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.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.designsystem.theme.placeholderBackground import io.element.android.libraries.matrix.api.core.RoomIdOrAlias -import io.element.android.libraries.matrix.ui.components.InviteSenderView +import io.element.android.libraries.matrix.api.room.join.JoinRule +import io.element.android.libraries.matrix.ui.components.SpaceInfoRow +import io.element.android.libraries.matrix.ui.components.SpaceMembersView +import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.persistentListOf @Composable fun JoinRoomView( @@ -92,7 +99,7 @@ fun JoinRoomView( containerColor = Color.Transparent, contentPadding = PaddingValues( horizontal = 16.dp, - vertical = 32.dp + vertical = 24.dp ), topBar = { JoinRoomTopBar( @@ -220,12 +227,14 @@ private fun JoinRoomFooter( onClick = { onDeclineInvite(joinAuthorisationStatus.inviteData, false) }, modifier = Modifier.weight(1f), size = ButtonSize.LargeLowPadding, + leadingIcon = IconSource.Vector(CompoundIcons.Close()) ) Button( text = stringResource(CommonStrings.action_accept), onClick = { onAcceptInvite(joinAuthorisationStatus.inviteData) }, modifier = Modifier.weight(1f), size = ButtonSize.LargeLowPadding, + leadingIcon = IconSource.Vector(CompoundIcons.Check()) ) } Spacer(modifier = Modifier.height(24.dp)) @@ -278,7 +287,6 @@ private fun JoinRoomFooter( JoinAuthorisationStatus.Unknown -> JoinRestrictedFooter(onJoinRoom) JoinAuthorisationStatus.Restricted -> JoinRestrictedFooter(onJoinRoom) JoinAuthorisationStatus.Unauthorized -> JoinUnauthorizedFooter(onGoBack) - is JoinAuthorisationStatus.IsSpace -> UnsupportedSpaceFooter(joinAuthorisationStatus.applicationName, onGoBack) JoinAuthorisationStatus.None -> Unit } } @@ -358,28 +366,6 @@ private fun JoinRestrictedFooter( } } -@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, @@ -397,19 +383,40 @@ private fun JoinRoomContent( IsKnockedLoadedContent() } else -> { - 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)) - } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { DefaultLoadedContent( - modifier = Modifier.verticalScroll(rememberScrollState()), contentState = contentState, - knockMessage = knockMessage, hideAvatarImage = hideAvatarsImages, - onKnockMessageUpdate = onKnockMessageUpdate ) + when (contentState.joinAuthorisationStatus) { + is JoinAuthorisationStatus.IsInvited -> { + val inviteSender = contentState.joinAuthorisationStatus.inviteSender + if (inviteSender != null) { + Spacer(Modifier.height(16.dp)) + InvitedByView(inviteSender, hideAvatarsImages) + } + } + is JoinAuthorisationStatus.CanKnock -> { + Spacer(modifier = Modifier.height(24.dp)) + val supportingText = if (knockMessage.isNotEmpty()) { + "${knockMessage.length}/$MAX_KNOCK_MESSAGE_LENGTH" + } else { + stringResource(R.string.screen_join_room_knock_message_description) + } + TextField( + value = knockMessage, + onValueChange = onKnockMessageUpdate, + maxLines = 3, + minLines = 3, + modifier = Modifier.fillMaxWidth(), + supportingText = supportingText + ) + } + else -> Unit + } } } } @@ -422,6 +429,45 @@ private fun JoinRoomContent( } } +@Composable +private fun InvitedByView( + sender: InviteSender, + hideAvatarImage: Boolean, + modifier: Modifier = Modifier +) { + Column( + modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.screen_join_room_invited_by), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary + ) + Spacer(Modifier.height(8.dp)) + Avatar( + avatarData = sender.avatarData, + avatarType = AvatarType.User, + hideImage = hideAvatarImage, + forcedAvatarSize = AvatarSize.RoomPreviewInviter.dp + ) + Spacer(Modifier.height(8.dp)) + Text( + text = sender.displayName, + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textPrimary + ) + Spacer(Modifier.height(4.dp)) + Text( + text = sender.userId.value, + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary + ) + } +} + @Composable private fun UnknownRoomContent( modifier: Modifier = Modifier @@ -429,7 +475,21 @@ private fun UnknownRoomContent( RoomPreviewOrganism( modifier = modifier, avatar = { - Spacer(modifier = Modifier.size(AvatarSize.RoomHeader.dp)) + Box( + modifier = Modifier + .size(AvatarSize.RoomPreviewHeader.dp) + .background( + color = ElementTheme.colors.placeholderBackground, + shape = CircleShape + ) + ) { + Icon( + modifier = Modifier.align(Alignment.Center), + tint = ElementTheme.colors.iconPrimary, + imageVector = CompoundIcons.VisibilityOff(), + contentDescription = null, + ) + } }, title = { RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview)) @@ -448,7 +508,7 @@ private fun IncompleteContent( RoomPreviewOrganism( modifier = modifier, avatar = { - PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) + PlaceholderAtom(width = AvatarSize.RoomPreviewHeader.dp, height = AvatarSize.RoomPreviewHeader.dp) }, title = { when (roomIdOrAlias) { @@ -471,43 +531,32 @@ private fun IncompleteContent( @Composable private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) { - BoxWithConstraints( - modifier = modifier - .fillMaxHeight() - .padding(horizontal = 16.dp), - contentAlignment = Alignment.Center, - ) { - IconTitleSubtitleMolecule( - modifier = Modifier.sizeIn(minHeight = maxHeight * 0.7f), - iconStyle = BigIcon.Style.SuccessSolid, - title = stringResource(R.string.screen_join_room_knock_sent_title), - subTitle = stringResource(R.string.screen_join_room_knock_sent_description), - ) - } + IconTitleSubtitleMolecule( + modifier = modifier.padding(horizontal = 8.dp), + iconStyle = BigIcon.Style.SuccessSolid, + title = stringResource(R.string.screen_join_room_knock_sent_title), + subTitle = stringResource(R.string.screen_join_room_knock_sent_description), + ) } @Composable private fun DefaultLoadedContent( contentState: ContentState.Loaded, - knockMessage: String, hideAvatarImage: Boolean, - onKnockMessageUpdate: (String) -> Unit, modifier: Modifier = Modifier, ) { RoomPreviewOrganism( modifier = modifier, avatar = { Avatar( - contentState.avatarData(AvatarSize.RoomHeader), + contentState.avatarData(AvatarSize.RoomPreviewHeader), hideImage = hideAvatarImage, - avatarType = AvatarType.Room(), + avatarType = if (contentState.isSpace) AvatarType.Space() else AvatarType.Room(), ) }, title = { if (contentState.name != null) { - RoomPreviewTitleAtom( - title = contentState.name, - ) + RoomPreviewTitleAtom(title = contentState.name) } else { RoomPreviewTitleAtom( title = stringResource(id = CommonStrings.common_no_room_name), @@ -516,37 +565,32 @@ private fun DefaultLoadedContent( } }, subtitle = { - if (contentState.alias != null) { - RoomPreviewSubtitleAtom(contentState.alias.value) - } - }, - description = { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - RoomPreviewDescriptionAtom(contentState.topic ?: "") - if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) { - Spacer(modifier = Modifier.height(24.dp)) - val supportingText = if (knockMessage.isNotEmpty()) { - "${knockMessage.length}/$MAX_KNOCK_MESSAGE_LENGTH" - } else { - stringResource(R.string.screen_join_room_knock_message_description) - } - TextField( - value = knockMessage, - onValueChange = onKnockMessageUpdate, - maxLines = 3, - minLines = 3, - modifier = Modifier.fillMaxWidth(), - supportingText = supportingText + when { + contentState.details is LoadedDetails.Space -> { + SpaceInfoRow( + joinRule = contentState.joinRule ?: JoinRule.Public, + numberOfRooms = contentState.details.childrenCount, ) } + contentState.alias != null -> { + RoomPreviewSubtitleAtom(contentState.alias.value) + } } }, + description = { + RoomPreviewDescriptionAtom( + contentState.topic ?: "", + maxLines = if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanJoin) Int.MAX_VALUE else 2 + ) + }, memberCount = { if (contentState.showMemberCount) { - RoomPreviewMembersCountMolecule(memberCount = contentState.numberOfMembers ?: 0) + val membersCount = contentState.numberOfMembers?.toInt() ?: 0 + if (contentState.isSpace) { + SpaceMembersView(persistentListOf(), membersCount) + } else { + MembersCountMolecule(memberCount = membersCount) + } } } ) 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 7826a819c6..675fb98a0c 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 @@ -7,18 +7,19 @@ package io.element.android.features.joinroom.impl.di -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 CancelKnockRoom { suspend operator fun invoke(roomId: RoomId): Result } @ContributesBinding(SessionScope::class) -class DefaultCancelKnockRoom @Inject constructor(private val client: MatrixClient) : CancelKnockRoom { +@Inject +class DefaultCancelKnockRoom(private val client: MatrixClient) : CancelKnockRoom { override suspend fun invoke(roomId: RoomId): Result { return client .getRoom(roomId) 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 index ac17e2c3e2..711439a44c 100644 --- 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 @@ -7,18 +7,19 @@ package io.element.android.features.joinroom.impl.di -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 { +@Inject +class DefaultForgetRoom(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 5e85c0abbc..a3a4a778f0 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 @@ -7,9 +7,9 @@ package io.element.android.features.joinroom.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState @@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.room.join.JoinRoom import java.util.Optional -@Module +@BindingContainer @ContributesTo(SessionScope::class) object JoinRoomModule { @Provides diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/KnockRoom.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/KnockRoom.kt index 471ef51a53..4d32043b0f 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/KnockRoom.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/KnockRoom.kt @@ -7,11 +7,11 @@ package io.element.android.features.joinroom.impl.di -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 javax.inject.Inject interface KnockRoom { suspend operator fun invoke( @@ -22,7 +22,8 @@ interface KnockRoom { } @ContributesBinding(SessionScope::class) -class DefaultKnockRoom @Inject constructor(private val client: MatrixClient) : KnockRoom { +@Inject +class DefaultKnockRoom(private val client: MatrixClient) : KnockRoom { override suspend fun invoke( roomIdOrAlias: RoomIdOrAlias, message: String, 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 2e0720ea42..254d6dc9f4 100644 --- a/features/joinroom/impl/src/main/res/values-cs/translations.xml +++ b/features/joinroom/impl/src/main/res/values-cs/translations.xml @@ -15,6 +15,7 @@ "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" "Abyste se mohli připojit k této místnosti, potřebujete pozvánku." + "Pozván(a)" "Připojit se do místnosti" "Abyste se mohli připojit, musíte být pozváni nebo být členem některého prostoru." "Zaklepejte a připojte se" diff --git a/features/joinroom/impl/src/main/res/values-cy/translations.xml b/features/joinroom/impl/src/main/res/values-cy/translations.xml index b093d94220..480ccdbb6c 100644 --- a/features/joinroom/impl/src/main/res/values-cy/translations.xml +++ b/features/joinroom/impl/src/main/res/values-cy/translations.xml @@ -18,6 +18,7 @@ "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" + "Nodau a ganiateir %1$d o %2$d" "Neges (dewisol)" "Byddwch yn derbyn gwahoddiad i ymuno â\'r ystafell os caiff eich cais ei dderbyn." "Anfonwyd y cais i ymuno" diff --git a/features/joinroom/impl/src/main/res/values-da/translations.xml b/features/joinroom/impl/src/main/res/values-da/translations.xml index ac260472e0..ff85698b23 100644 --- a/features/joinroom/impl/src/main/res/values-da/translations.xml +++ b/features/joinroom/impl/src/main/res/values-da/translations.xml @@ -15,6 +15,7 @@ "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" + "Inviteret af" "Deltag i rummet" "Du skal muligvis være inviteret eller være medlem af en klynge for at deltage." "Send anmodning om at deltage" 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 0fa2bdce5f..7971edbeff 100644 --- a/features/joinroom/impl/src/main/res/values-de/translations.xml +++ b/features/joinroom/impl/src/main/res/values-de/translations.xml @@ -1,32 +1,34 @@ - "Sie wurden von %1$s für diesen Chatroom gesperrt." - "Sie wurden für diesen Chatroom gesperrt" + "Du wurdest von %1$s für diesen Chat gesperrt." + "Du wurdest für diesen Chat gesperrt" "Grund:%1$s." "Anfrage abbrechen" "Ja, abbrechen" - "Möchten Sie Ihre Beitrittsanfrage für diesen Chatroom wirklich stornieren?" - "Beitrittsanfrage stornieren" + "Willst du wirklich deine Anfrage zum Beitritt zu diesem Chat abbrechen?" + "Beitrittsanfrage abbrechen" "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." + "Bist du sicher, dass du die Einladung zu diesem Chat ablehnen möchtest? Dadurch wird auch jede weitere Kontaktaufnahme oder Chat Einladung von %1$s blockiert." "Einladung ablehnen & Nutzer blockieren" "Ablehnen und blockieren" - "Der Beitritt zum Chatroom schlug fehl." - "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 Chatroom zu betreten" - "Raum beitreten" - "Möglicherweise müssen Sie eingeladen sein oder Mitglied eines Spaces sein, um beitreten zu können." + "Der Beitritt zum Chat schlug fehl." + "Dieser Chat ist entweder nur auf Einladung zugänglich oder es gibt andere Zugangsbeschränkungen durch Spaces." + "Vergiss diesen Chat" + "Du benötigst eine Einladung, um diesem Chat beizutreten" + "Eingeladen von" + "Chat beitreten" + "Möglicherweise musst du eingeladen werden oder ein Mitglied eines Spaces sein, um beitreten zu können." "Anklopfen" + "%1$d von %2$d erlaubte Zeichen" "Nachricht (optional)" - "Falls Ihre Anfrage, den Raum zu betreten, akzeptiert wird, erhalten Sie eine Einladung." + "Sollte deine Anfrage akzeptiert werden, erhältst du eine Einladung, dem Chat beizutreten." "Beitrittsanfrage geschickt" - "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." + "Wir konnten die Chat Vorschau nicht anzeigen. Dies kann an Netzwerk- oder Serverproblemen liegen." + "Wir konnten diese Chat-Vorschau nicht anzeigen" + "%1$s unterstützt noch keine Spaces. Du kannst auf Spaces im Web zugreifen." "Spaces werden noch nicht unterstützt" - "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?" + "Klopfe an um einen Admin zu benachrichtigen. Nach der Freigabe kannst du dich an der Unterhaltung beteiligen." + "Du musst Mitglied in diesem Chat sein, um den Nachrichtenverlauf zu sehen." + "Willst du diesem Chat beitreten?" "Vorschau nicht verfügbar" 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 b69ca16d26..ce4db056f1 100644 --- a/features/joinroom/impl/src/main/res/values-et/translations.xml +++ b/features/joinroom/impl/src/main/res/values-et/translations.xml @@ -15,6 +15,7 @@ "Ligipääs siia jututuppa on võimalik vaid kutse alusel või kehtivad siin kogukonnakohased piirangud." "Unusta see jututuba" "Selle jututoaga liitumiseks vajad sa kutset" + "Kutsuja" "Liitu jututoaga" "Selle jututoaga liitumiseks sa vajad kutset või pead juba olema kogukonna liige." "Liitumiseks koputa jututoa uksele" 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 5c200b62dd..e0066fa2ad 100644 --- a/features/joinroom/impl/src/main/res/values-fr/translations.xml +++ b/features/joinroom/impl/src/main/res/values-fr/translations.xml @@ -15,6 +15,7 @@ "Ce salon est accessible uniquement sur invitation ou il peut y avoir des restrictions d’accès au niveau de l’espace." "Oublier ce salon" "Vous avez besoin d’une invitation pour rejoindre ce salon" + "Invité(e) par" "Rejoindre" "Il est possible que vous deviez être invité ou être membre d’un Espace pour pouvoir rejoindre le salon." "Demander à joindre" 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 6a3f9b836f..f879da46f0 100644 --- a/features/joinroom/impl/src/main/res/values-hu/translations.xml +++ b/features/joinroom/impl/src/main/res/values-hu/translations.xml @@ -15,6 +15,7 @@ "Ebbe a szobába csak meghívóval vagy tértagsággal lehet belépni." "Szoba elfelejtése" "Meghívóra van szüksége ahhoz, hogy csatlakozzon ehhez a szobához" + "Meghívta:" "Csatlakozás a szobához" "A csatlakozáshoz meghívásra vagy tértagságra lehet szüksége." "Kopogtasson a csatlakozáshoz" 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 753b54159a..1ea811d8dc 100644 --- a/features/joinroom/impl/src/main/res/values-it/translations.xml +++ b/features/joinroom/impl/src/main/res/values-it/translations.xml @@ -18,6 +18,7 @@ "Entra nella stanza" "Potrebbe essere necessario essere invitati o essere membro di uno spazio per partecipare." "Bussa per partecipare" + "Caratteri consentiti: %1$d di %2$d" "Messaggio (opzionale)" "Riceverai un invito a entrare nella stanza se la tua richiesta viene accettata." "Richiesta di accesso inviata" diff --git a/features/joinroom/impl/src/main/res/values-ko/translations.xml b/features/joinroom/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..b6edc8447e --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,33 @@ + + + "%1$s 에 의해 이 방에서 퇴장당했습니다." + "당신은 이 방에서 차단되었습니다" + "이유: %1$s." + "요청 취소" + "네, 취소합니다" + "이 방에 대한 가입 요청을 정말로 취소하시겠습니까?" + "가입 요청 취소" + "예, 거부 및 차단" + "이 방에 대한 초대 거부를 정말로 확인하시겠습니까? 이 경우 %1$s 에서 귀하에게 연락하거나 방에 초대할 수 없게 됩니다." + "초대 거부 및 차단" + "거부 및 차단" + "방에 참여하는데 실패했습니다." + "이 방은 초대 전용이거나 스페이스 수준에서 액세스 제한이 있을 수 있습니다." + "이 방 지우기" + "이 방에 참여하려면 초대장이 필요합니다." + "방에 참여하기" + "참여하려면 초대 또는 스페이스의 회원이어야 할 수 있습니다." + "가입 요청 보내기" + "%2$d의 %1$d 문자가 허용됨" + "메시지 (선택 사항)" + "요청이 승인되면 방에 참여하기 위한 초대장이 발송됩니다." + "가입 요청이 전송되었습니다" + "방 미리보기를 표시할 수 없습니다. 네트워크 또는 서버 문제 때문일 수 있습니다." + "이 방 미리보기를 표시할 수 없습니다." + "%1$s 아직 스페이스를 지원하지 않습니다. 웹에서 스페이스에 접속할 수 있습니다." + "아직 스페이스가 지원되지 않습니다." + "아래 버튼을 클릭하면 방 관리자에게 알림이 전송됩니다. 승인이 완료되면 대화에 참여할 수 있습니다." + "이 방의 회원이어야만 메시지 기록을 볼 수 있습니다." + "이 방에 참여하고 싶으신가요?" + "미리보기 기능은 제공되지 않습니다." + 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 f1f4d7afda..a349def982 100644 --- a/features/joinroom/impl/src/main/res/values-nb/translations.xml +++ b/features/joinroom/impl/src/main/res/values-nb/translations.xml @@ -18,6 +18,7 @@ "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" + "Tillatte tegn %1$d av %2$d" "Melding (valgfritt)" "Du vil motta en invitasjon til å bli med i rommet hvis forespørselen din blir akseptert." "Forespørsel om å bli med sendt" 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 015e235c0e..38f91512d7 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 @@ -4,27 +4,28 @@ "Você foi banido desta sala" "Motivo: %1$s." "Cancelar pedido" - "Sim, cancele" + "Sim, cancelar" "Tem a certeza de que pretende cancelar o seu pedido de adesão a esta sala?" - "Cancelar pedido de adesão" + "Cancelar pedido de entrada" "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." + "Tem certeza de que quer recusar o convite para entrar nesta sala? Isso também impedirá que %1$s entre em contato com você ou o convide para 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." + "Esta sala é apenas para convidados ou pode haver restrições de acesso a 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" + "Enviar solicitação para entrar" + "%1$d de %2$d caráteres permitidos" "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" + "Você receberá um convite para entrar nesta sala se seu pedido for aceito." + "Pedido de entrada enviado" + "Não foi possível exibir a pré-visualização da sala. Isso pode ser devido a problemas de rede ou do servidor." + "Não foi possível exibir a pré-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" + "Ainda não há suporte aos espaços" "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?" 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 695d3124c3..3bb729bef6 100644 --- a/features/joinroom/impl/src/main/res/values-pt/translations.xml +++ b/features/joinroom/impl/src/main/res/values-pt/translations.xml @@ -15,12 +15,13 @@ "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" + "Convidado por" "Entrar na sala" "Podes ter que ser convidado ou pertenceres a um espaço para poderes entrar." "Bater à porta" "%1$d de %2$d caracteres permitidos" "Mensagem (opcional)" - "Irá receber um convite para participar na sala se seu pedido for aceite." + "Irás receber um convite para participar na sala se o 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" diff --git a/features/joinroom/impl/src/main/res/values-ro/translations.xml b/features/joinroom/impl/src/main/res/values-ro/translations.xml index fb27555bdf..ca7764aa62 100644 --- a/features/joinroom/impl/src/main/res/values-ro/translations.xml +++ b/features/joinroom/impl/src/main/res/values-ro/translations.xml @@ -1,18 +1,33 @@ - "Anulați solicitarea" + "Ai fost exclus din această cameră de către %1$s." + "Ați fost exclus din această cameră." + "Motiv: %1$s." + "Anulați cererea" "Da, anulați" - "Sunteți sigur că doriți să anulați solicitarea de a vă alătura acestei camere?" + "Sunteți sigur că doriți să anulați cererea de a vă alătura acestei camere?" "Anulați cererea de alăturare" + "Da, refuzați și blocați" + "Sunteți sigur că doriți să refuzați invitația de a vă alătura acestei camere? Acest lucru va împiedica, de asemenea, %1$s să vă contacteze sau să vă invite în camere." + "Refuzați invitația și blocați" + "Refuzați și blocați" + "Alăturarea la cameră a eșuat." + "Această cameră este fie accesibilă numai pe bază de invitație, fie exista restricții de acces la nivel de spațiu." + "Uitați această cameră" + "Aveți nevoie de o invitație pentru a vă alătura acestei camere." "Alăturați-vă camerei" - "Bateți pentru a vă alătura" + "Este posibil să fie necesar să fiți invitat sau să fiți membru al unui spațiu pentru a vă alătura." + "Trimiteți o cerere de alăturare" + "Caractere permise %1$d din %2$d" "Mesaj (opțional)" "Veți primi o invitație de a vă alătura camerei dacă cererea dumneavoastră este acceptată." "Cererea de alăturare a fost trimisă" + "Nu am putut afișa previzualizarea camerei. Este posibil ca acest lucru să se datoreze unor probleme de rețea sau de server." + "Nu am putut afișa previzualizarea acestei camere." "%1$s nu suporta încă spații. Puteți accesa spațiile pe web." "Spațiile nu sunt încă suportate" "Faceți clic pe butonul de mai jos și un administrator de cameră va fi notificat. Veți putea să vă alăturați conversației odată aprobată." - "Trebuie să fiți membru al acestei camere pentru a vizualiza istoricul mesajelor." + "Trebuie să fiți membru al acestei camere pentru a vizualiza mesajele anterioare." "Doriți să vă alăturați acestei camere?" "Previzualizare indisponibilă" 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 58a94019c1..4c87355000 100644 --- a/features/joinroom/impl/src/main/res/values-ru/translations.xml +++ b/features/joinroom/impl/src/main/res/values-ru/translations.xml @@ -17,7 +17,7 @@ "Вам необходимо приглашение для того, чтобы присоединиться к этой комнате" "Присоединиться к комнате" "Чтобы присоединиться, вам необходимо приглашение или быть участником сообщества." - "Постучите, чтобы присоединиться" + "Отправить запрос на присоединение" "Сообщение (опционально)" "Вы получите приглашение присоединиться к комнате, как только ваш запрос будет принят." "Запрос на присоединение отправлен" 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 a6aa79ebd9..78924f1cd6 100644 --- a/features/joinroom/impl/src/main/res/values-sv/translations.xml +++ b/features/joinroom/impl/src/main/res/values-sv/translations.xml @@ -18,6 +18,7 @@ "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" + "Tillåtna tecken %1$d av %2$d" "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" diff --git a/features/joinroom/impl/src/main/res/values-uz/translations.xml b/features/joinroom/impl/src/main/res/values-uz/translations.xml new file mode 100644 index 0000000000..2fbb5e38d7 --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-uz/translations.xml @@ -0,0 +1,18 @@ + + + "So‘rovni bekor qilish" + "Ha, bekor qiling" + "Bu xonaga qo‘shilish so‘rovingizni bekor qilishni xohlayotganingizga ishonchingiz komilmi?" + "Qo‘shilish so‘rovini bekor qilish" + "Xonaga qoʻshilish" + "Qoʻshilish soʻrovini yuborish" + "Xabar (ixtiyoriy)" + "Agar so‘rovingiz qabul qilinsa, xonaga qo‘shilish taklifini olasiz." + "Qo‘shilish so‘rovi yuborildi" + "%1$s hali maydon xizmatini qoʻllab-quvvatlamaydi. maydonga veb-sayt orqali kirishingiz mumkin." + "Maydonlar hali qoʻllab-quvvatlanmaydi" + "Quyidagi tugmani bosing va xona administratoriga xabar beriladi. Ruxsat berilgandan soʻng suhbatga qoʻshilishingiz mumkin boʻladi." + "Xabarlar tarixini koʻrish uchun siz ushbu xonaning aʼzosi boʻlishingiz shart." + "Bu xonaga qoʻshilishni xohlaysizmi?" + "Oldindan koʻrish imkoni yoʻq" + diff --git a/features/joinroom/impl/src/main/res/values-zh/translations.xml b/features/joinroom/impl/src/main/res/values-zh/translations.xml index 50a4c65028..05c52c9293 100644 --- a/features/joinroom/impl/src/main/res/values-zh/translations.xml +++ b/features/joinroom/impl/src/main/res/values-zh/translations.xml @@ -18,6 +18,7 @@ "加入聊天室" "您可能需要受到邀请或成为某个空间的成员才能加入。" "加入聊天室" + "允许的字符数量 %2$d中的%1$d" "消息(可选)" "如果您的请求被接受,您将收到加入房间的邀请。" "加入请求已发送" diff --git a/features/joinroom/impl/src/main/res/values/localazy.xml b/features/joinroom/impl/src/main/res/values/localazy.xml index f08807bd40..3d6a319aa6 100644 --- a/features/joinroom/impl/src/main/res/values/localazy.xml +++ b/features/joinroom/impl/src/main/res/values/localazy.xml @@ -15,6 +15,7 @@ "This room is either invite-only or there might be restrictions to access at space level." "Forget this room" "You need an invite in order to join this room" + "Invited by" "Join room" "You may need to be invited or be a member of a space in order to join." "Send request to join" diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPointTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPointTest.kt new file mode 100644 index 0000000000..af75fd528c --- /dev/null +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPointTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint +import io.element.android.features.joinroom.api.JoinRoomEntryPoint +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test +import java.util.Optional + +class DefaultJoinRoomEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultJoinRoomEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + JoinRoomFlowNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { _, _, _, _, _ -> createJoinRoomPresenter() }, + acceptDeclineInviteView = { _, _, _, _ -> lambdaError() }, + declineAndBlockEntryPoint = object : DeclineInviteAndBlockEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData) = lambdaError() + } + ) + } + val inputs = JoinRoomEntryPoint.Inputs( + roomId = A_ROOM_ID, + roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(), + roomDescription = Optional.ofNullable(null), + serverNames = emptyList(), + trigger = JoinedRoom.Trigger.RoomDirectory, + ) + val result = entryPoint.createNode(parentNode, BuildContext.root(null), inputs) + assertThat(result).isInstanceOf(JoinRoomFlowNode::class.java) + assertThat(result.plugins).contains(inputs) + } +} 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 8a475668ae..10b5c1a712 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 @@ -13,6 +13,7 @@ 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.acceptdecline.anAcceptDeclineInviteState 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 @@ -28,13 +29,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.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.join.JoinRoom import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -50,14 +49,19 @@ import io.element.android.libraries.matrix.test.room.aRoomMember 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.test.spaces.FakeSpaceRoomList +import io.element.android.libraries.matrix.test.spaces.FakeSpaceService +import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender +import io.element.android.libraries.previewutils.room.aSpaceRoom import io.element.android.tests.testutils.WarmUpRule 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.collections.immutable.persistentListOf import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -89,6 +93,9 @@ class JoinRoomPresenterTest { val roomInfo = aRoomInfo() val matrixClient = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ).apply { getRoomInfoFlowLambda = { _ -> flowOf(Optional.of(roomInfo)) @@ -106,7 +113,7 @@ class JoinRoomPresenterTest { 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.details).isEqualTo(aLoadedDetailsRoom(isDm = roomInfo.isDirect)) assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl) } } @@ -117,6 +124,9 @@ class JoinRoomPresenterTest { val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED) val matrixClient = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ).apply { getRoomInfoFlowLambda = { _ -> flowOf(Optional.of(roomInfo)) @@ -141,7 +151,7 @@ class JoinRoomPresenterTest { @Test 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 inviter = aRoomMember(userId = A_USER_ID_2, displayName = "Bob") val expectedInviteSender = inviter.toInviteSender() val roomInfo = aRoomInfo( currentUserMembership = CurrentUserMembership.INVITED, @@ -150,7 +160,21 @@ class JoinRoomPresenterTest { ) val inviteData = roomInfo.toInviteData() val matrixClient = FakeMatrixClient( - getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview( + info = aRoomPreviewInfo( + numberOfJoinedMembers = 5, + ), + roomMembershipDetails = { + Result.success(aRoomMembershipDetails()) + }, + ) + ) + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ).apply { getRoomInfoFlowLambda = { _ -> flowOf(Optional.of(roomInfo)) @@ -168,6 +192,137 @@ class JoinRoomPresenterTest { } } + @Test + fun `present - when space is invited then join authorization is equal to invited, an inviter is provided`() = runTest { + val inviter = aRoomMember(userId = A_USER_ID_2, displayName = "Bob") + val expectedInviteSender = inviter.toInviteSender() + val spaceHero = aMatrixUser() + val roomInfo = aRoomInfo( + isSpace = true, + currentUserMembership = CurrentUserMembership.INVITED, + joinedMembersCount = 5, + inviter = inviter, + heroes = listOf(spaceHero), + ) + val inviteData = roomInfo.toInviteData() + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview( + info = aRoomPreviewInfo( + numberOfJoinedMembers = 5, + ), + roomMembershipDetails = { + Result.success(aRoomMembershipDetails()) + }, + ) + ) + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { + FakeSpaceRoomList( + initialSpaceFlowValue = aSpaceRoom( + childrenCount = 3, + ) + ) + }, + ), + ).apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.of(roomInfo)) + } + } + val presenter = createJoinRoomPresenter( + matrixClient = matrixClient + ) + presenter.test { + skipItems(2) + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(inviteData, expectedInviteSender)) + assertThat((state.contentState as ContentState.Loaded).numberOfMembers).isEqualTo(5) + // Space details are provided + assertThat(state.contentState.details).isEqualTo( + LoadedDetails.Space( + childrenCount = 3, + heroes = persistentListOf(spaceHero), + ) + ) + } + } + } + + @Test + fun `present - space is invited - no room info`() = runTest { + val spaceHero = aMatrixUser() + val spaceRoom = aSpaceRoom( + childrenCount = 3, + heroes = listOf(spaceHero), + ) + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.failure(Exception("Error")) + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { + FakeSpaceRoomList( + initialSpaceFlowValue = spaceRoom, + ) + }, + ), + ).apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.ofNullable(null)) + } + } + val presenter = createJoinRoomPresenter( + matrixClient = matrixClient + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + // Space details are provided + assertThat((state.contentState as ContentState.Loaded).details).isEqualTo( + LoadedDetails.Space( + childrenCount = 3, + heroes = persistentListOf(spaceHero), + ) + ) + } + } + } + + @Test + fun `present - space is invited - no room info - space room state set`() = runTest { + val spaceRoom = aSpaceRoom( + state = CurrentUserMembership.INVITED, + ) + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.failure(Exception("Error")) + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { + FakeSpaceRoomList( + initialSpaceFlowValue = spaceRoom, + ) + }, + ), + ).apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.ofNullable(null)) + } + } + val presenter = createJoinRoomPresenter( + matrixClient = matrixClient + ) + presenter.test { + awaitItem().also { state -> + // Space details are provided + assertThat(state.contentState).isInstanceOf(ContentState.Loading::class.java) + } + } + } + @Test fun `present - when room is invited read the number of member from the room preview`() = runTest { val roomInfo = aRoomInfo( @@ -181,10 +336,16 @@ class JoinRoomPresenterTest { aRoomPreview( info = aRoomPreviewInfo( numberOfJoinedMembers = 10, - ) + ), + roomMembershipDetails = { + Result.success(aRoomMembershipDetails()) + }, ) ) }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ).apply { getRoomInfoFlowLambda = { _ -> flowOf(Optional.of(roomInfo)) @@ -208,7 +369,11 @@ class JoinRoomPresenterTest { anAcceptDeclineInviteState(eventSink = eventSinkRecorder) } val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED) - val matrixClient = FakeMatrixClient().apply { + val matrixClient = FakeMatrixClient( + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), + ).apply { getRoomInfoFlowLambda = { _ -> flowOf(Optional.of(roomInfo)) } @@ -243,6 +408,9 @@ class JoinRoomPresenterTest { } val matrixClient = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = matrixClient, @@ -271,6 +439,9 @@ class JoinRoomPresenterTest { fun `present - when room is joined with error, it is possible to clear the error`() = runTest { val matrixClient = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = matrixClient, @@ -333,16 +504,14 @@ class JoinRoomPresenterTest { currentUserMembership = CurrentUserMembership.BANNED, ), roomMembershipDetails = { - Result.success( - RoomMembershipDetails( - currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"), - senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"), - ) - ) + Result.success(aRoomMembershipDetails()) } ) ) - } + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ).apply { getRoomInfoFlowLambda = { _ -> flowOf(Optional.of(roomInfo)) @@ -370,6 +539,9 @@ class JoinRoomPresenterTest { val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, joinRule = JoinRule.Public) val matrixClient = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ).apply { getRoomInfoFlowLambda = { _ -> flowOf(Optional.of(roomInfo)) @@ -391,6 +563,9 @@ class JoinRoomPresenterTest { val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, joinRule = null) val matrixClient = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ).apply { getRoomInfoFlowLambda = { _ -> flowOf(Optional.of(roomInfo)) @@ -422,7 +597,7 @@ class JoinRoomPresenterTest { assertThat(contentState.topic).isEqualTo(roomDescription.topic) assertThat(contentState.alias).isEqualTo(roomDescription.alias) assertThat(contentState.numberOfMembers).isEqualTo(roomDescription.numberOfMembers) - assertThat(contentState.isDm).isFalse() + assertThat(contentState.details).isEqualTo(aLoadedDetailsRoom(isDm = false)) assertThat(contentState.roomAvatarUrl).isEqualTo(roomDescription.avatarUrl) } } @@ -496,6 +671,9 @@ class JoinRoomPresenterTest { val fakeKnockRoom = FakeKnockRoom(knockRoomSuccess) val matrixClient = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = matrixClient, @@ -541,6 +719,9 @@ class JoinRoomPresenterTest { val cancelKnockRoom = FakeCancelKnockRoom(cancelKnockRoomSuccess) val matrixClient = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = matrixClient, @@ -585,6 +766,9 @@ class JoinRoomPresenterTest { val fakeForgetRoom = FakeForgetRoom(forgetRoomSuccess) val matrixClient = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = matrixClient, @@ -633,10 +817,16 @@ class JoinRoomPresenterTest { isHistoryWorldReadable = false, joinRule = JoinRule.Public, currentUserMembership = null, - ) + ), + roomMembershipDetails = { + Result.success(aRoomMembershipDetails()) + }, ) ) - } + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = client @@ -651,10 +841,10 @@ class JoinRoomPresenterTest { topic = "Room topic", alias = RoomAlias("#alias:matrix.org"), numberOfMembers = 2, - isDm = false, - roomType = RoomType.Room, roomAvatarUrl = "avatarUrl", - joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin + joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin, + joinRule = JoinRule.Public, + details = aLoadedDetailsRoom(isDm = false), ) ) } @@ -680,16 +870,14 @@ class JoinRoomPresenterTest { currentUserMembership = CurrentUserMembership.INVITED, ), roomMembershipDetails = { - Result.success( - RoomMembershipDetails( - currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"), - senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"), - ) - ) + Result.success(aRoomMembershipDetails()) } ) ) - } + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = client @@ -704,8 +892,6 @@ class JoinRoomPresenterTest { topic = "Room topic", alias = RoomAlias("#alias:matrix.org"), numberOfMembers = 2, - isDm = false, - roomType = RoomType.Room, roomAvatarUrl = "avatarUrl", joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited( inviteData = InviteData( @@ -723,7 +909,9 @@ class JoinRoomPresenterTest { ), membershipChangeReason = null, ), - ) + ), + joinRule = JoinRule.Public, + details = aLoadedDetailsRoom(isDm = false), ) ) } @@ -750,15 +938,15 @@ class JoinRoomPresenterTest { ), roomMembershipDetails = { Result.success( - RoomMembershipDetails( - currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"), - senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"), - ) + aRoomMembershipDetails(), ) } ) ) - } + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = client @@ -773,8 +961,6 @@ class JoinRoomPresenterTest { topic = "Room topic", alias = RoomAlias("#alias:matrix.org"), numberOfMembers = 2, - isDm = false, - roomType = RoomType.Room, roomAvatarUrl = "avatarUrl", joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned( banSender = InviteSender( @@ -788,7 +974,9 @@ class JoinRoomPresenterTest { membershipChangeReason = null, ), reason = null, - ) + ), + joinRule = JoinRule.Public, + details = aLoadedDetailsRoom(isDm = false), ) ) } @@ -814,16 +1002,14 @@ class JoinRoomPresenterTest { currentUserMembership = CurrentUserMembership.KNOCKED, ), roomMembershipDetails = { - Result.success( - RoomMembershipDetails( - currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"), - senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"), - ) - ) + Result.success(aRoomMembershipDetails()) } ) ) - } + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = client @@ -838,10 +1024,10 @@ class JoinRoomPresenterTest { topic = "Room topic", alias = RoomAlias("#alias:matrix.org"), numberOfMembers = 2, - isDm = false, - roomType = RoomType.Room, roomAvatarUrl = "avatarUrl", - joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked + joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked, + joinRule = JoinRule.Public, + details = aLoadedDetailsRoom(isDm = false), ) ) } @@ -853,9 +1039,17 @@ class JoinRoomPresenterTest { val client = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.success( - aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Private)) + aRoomPreview( + info = aRoomPreviewInfo(joinRule = JoinRule.Private), + roomMembershipDetails = { + Result.success(aRoomMembershipDetails()) + }, + ) ) - } + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = client @@ -873,9 +1067,17 @@ class JoinRoomPresenterTest { val client = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.success( - aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Custom("custom"))) + aRoomPreview( + info = aRoomPreviewInfo(joinRule = JoinRule.Custom("custom")), + roomMembershipDetails = { + Result.success(aRoomMembershipDetails()) + }, + ) ) - } + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = client @@ -893,9 +1095,17 @@ class JoinRoomPresenterTest { val client = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.success( - aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Invite)) + aRoomPreview( + info = aRoomPreviewInfo(joinRule = JoinRule.Invite), + roomMembershipDetails = { + Result.success(aRoomMembershipDetails()) + }, + ) ) - } + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = client @@ -913,9 +1123,19 @@ class JoinRoomPresenterTest { val client = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.success( - aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.KnockRestricted(emptyList()))) + aRoomPreview( + info = aRoomPreviewInfo( + joinRule = JoinRule.KnockRestricted(persistentListOf()) + ), + roomMembershipDetails = { + Result.success(aRoomMembershipDetails()) + } + ) ) - } + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = client @@ -933,9 +1153,17 @@ class JoinRoomPresenterTest { val client = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.success( - aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Restricted(emptyList()))) + aRoomPreview( + info = aRoomPreviewInfo(joinRule = JoinRule.Restricted(persistentListOf())), + roomMembershipDetails = { + Result.success(aRoomMembershipDetails()) + }, + ) ) - } + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = client @@ -948,32 +1176,15 @@ class JoinRoomPresenterTest { } } - @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) - } + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = client @@ -1003,7 +1214,10 @@ class JoinRoomPresenterTest { val client = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) - } + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = client @@ -1028,7 +1242,10 @@ class JoinRoomPresenterTest { val client = FakeMatrixClient( getNotJoinedRoomResult = { _, _ -> Result.failure(ClientException.MatrixApi(ErrorKind.Forbidden, "403", "Forbidden", null)) - } + }, + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), ) val presenter = createJoinRoomPresenter( matrixClient = client @@ -1041,39 +1258,6 @@ class JoinRoomPresenterTest { } } - private fun createJoinRoomPresenter( - roomId: RoomId = A_ROOM_ID, - roomDescription: Optional = Optional.empty(), - serverNames: List = emptyList(), - trigger: JoinedRoom.Trigger = JoinedRoom.Trigger.Invite, - matrixClient: MatrixClient = FakeMatrixClient(), - joinRoomLambda: (RoomIdOrAlias, List, JoinedRoom.Trigger) -> Result = { _, _, _ -> - Result.success(Unit) - }, - knockRoom: KnockRoom = FakeKnockRoom(), - cancelKnockRoom: CancelKnockRoom = FakeCancelKnockRoom(), - forgetRoom: ForgetRoom = FakeForgetRoom(), - buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"), - acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, - seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(), - ): JoinRoomPresenter { - return JoinRoomPresenter( - roomId = roomId, - roomIdOrAlias = roomId.toRoomIdOrAlias(), - roomDescription = roomDescription, - serverNames = serverNames, - trigger = trigger, - matrixClient = matrixClient, - joinRoom = FakeJoinRoom(joinRoomLambda), - knockRoom = knockRoom, - cancelKnockRoom = cancelKnockRoom, - forgetRoom = forgetRoom, - buildMeta = buildMeta, - acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, - seenInvitesStore = seenInvitesStore, - ) - } - private fun aRoomDescription( roomId: RoomId = A_ROOM_ID, name: String? = A_ROOM_NAME, @@ -1094,3 +1278,45 @@ class JoinRoomPresenterTest { ) } } + +internal fun createJoinRoomPresenter( + roomId: RoomId = A_ROOM_ID, + roomDescription: Optional = Optional.empty(), + serverNames: List = emptyList(), + trigger: JoinedRoom.Trigger = JoinedRoom.Trigger.Invite, + matrixClient: MatrixClient = FakeMatrixClient( + spaceService = FakeSpaceService( + spaceRoomListResult = { FakeSpaceRoomList() }, + ), + ), + joinRoomLambda: (RoomIdOrAlias, List, JoinedRoom.Trigger) -> Result = { _, _, _ -> + Result.success(Unit) + }, + knockRoom: KnockRoom = FakeKnockRoom(), + cancelKnockRoom: CancelKnockRoom = FakeCancelKnockRoom(), + forgetRoom: ForgetRoom = FakeForgetRoom(), + buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"), + acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, + seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(), +): JoinRoomPresenter { + return JoinRoomPresenter( + roomId = roomId, + roomIdOrAlias = roomId.toRoomIdOrAlias(), + roomDescription = roomDescription, + serverNames = serverNames, + trigger = trigger, + matrixClient = matrixClient, + joinRoom = FakeJoinRoom(joinRoomLambda), + knockRoom = knockRoom, + cancelKnockRoom = cancelKnockRoom, + forgetRoom = forgetRoom, + buildMeta = buildMeta, + acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, + seenInvitesStore = seenInvitesStore, + ) +} + +private fun aRoomMembershipDetails() = RoomMembershipDetails( + currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"), + senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"), +) 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 0205c1cac0..f3487a43b6 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 @@ -14,7 +14,6 @@ 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 @@ -218,21 +217,6 @@ class JoinRoomViewTest { eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent) } - @Test - fun `clicking on ok when a space is displayed invokes the expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { - rule.setJoinRoomView( - aJoinRoomState( - contentState = aLoadedContentState(roomType = RoomType.Space), - eventSink = eventsRecorder, - ), - onBackClick = it - ) - rule.clickOn(CommonStrings.action_ok) - } - } - @Test fun `clicking on ok when user is unauthorized the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) diff --git a/features/knockrequests/impl/build.gradle.kts b/features/knockrequests/impl/build.gradle.kts index 39004b30e0..41fadb00da 100644 --- a/features/knockrequests/impl/build.gradle.kts +++ b/features/knockrequests/impl/build.gradle.kts @@ -5,7 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies plugins { id("io.element.android-compose-library") @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.knockrequests.api) @@ -33,15 +34,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.androidx.compose.ui.test.junit) testImplementation(projects.libraries.featureflag.test) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/DefaultKnockRequestsBannerRenderer.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/DefaultKnockRequestsBannerRenderer.kt index 994ba025a0..612ddc5a6a 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/DefaultKnockRequestsBannerRenderer.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/DefaultKnockRequestsBannerRenderer.kt @@ -9,13 +9,14 @@ package io.element.android.features.knockrequests.impl.banner import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.knockrequests.api.banner.KnockRequestsBannerRenderer import io.element.android.libraries.di.RoomScope -import javax.inject.Inject @ContributesBinding(RoomScope::class) -class DefaultKnockRequestsBannerRenderer @Inject constructor( +@Inject +class DefaultKnockRequestsBannerRenderer( private val presenter: KnockRequestsBannerPresenter, ) : KnockRequestsBannerRenderer { @Composable 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 737a3b0562..a1db930500 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 @@ -14,6 +14,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import dev.zacsweers.metro.Inject import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable import io.element.android.features.knockrequests.impl.data.KnockRequestsService import io.element.android.libraries.architecture.Presenter @@ -23,11 +24,11 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import javax.inject.Inject private const val ACCEPT_ERROR_DISPLAY_DURATION = 1500L -class KnockRequestsBannerPresenter @Inject constructor( +@Inject +class KnockRequestsBannerPresenter( private val knockRequestsService: KnockRequestsService, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, 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 334bb531ae..2ee070d41c 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 @@ -38,6 +38,7 @@ 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.AvatarRow 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 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 671208e97b..8236222f07 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 @@ -7,16 +7,16 @@ package io.element.android.features.knockrequests.impl.data -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn import io.element.android.libraries.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.JoinedRoom -@Module +@BindingContainer @ContributesTo(RoomScope::class) object KnockRequestsModule { @Provides diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPoint.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPoint.kt index e5181be27c..675f3bee9e 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPoint.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPoint.kt @@ -9,14 +9,15 @@ package io.element.android.features.knockrequests.impl.list import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultKnockRequestsListEntryPoint @Inject constructor() : KnockRequestsListEntryPoint { +@Inject +class DefaultKnockRequestsListEntryPoint : KnockRequestsListEntryPoint { override fun createNode(parentNode: Node, buildContext: BuildContext): Node { return parentNode.createNode(buildContext) } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListNode.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListNode.kt index 52cc5eb928..a9bec1556b 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListNode.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListNode.kt @@ -12,13 +12,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.RoomScope @ContributesNode(RoomScope::class) -class KnockRequestsListNode @AssistedInject constructor( +@AssistedInject +class KnockRequestsListNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: KnockRequestsListPresenter, diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt index aa537384b9..ff943fcf6b 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt @@ -16,15 +16,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.knockrequests.impl.data.KnockRequestsService import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import javax.inject.Inject -class KnockRequestsListPresenter @Inject constructor( +@Inject +class KnockRequestsListPresenter( private val knockRequestsService: KnockRequestsService, ) : Presenter { @Composable 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 45df2ecc8f..33e9e1be70 100644 --- a/features/knockrequests/impl/src/main/res/values-de/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-de/translations.xml @@ -1,36 +1,36 @@ "Ja, akzeptiere alle" - "Sind Sie sicher, dass Sie alle Beitrittsanfragen akzeptieren möchten?" - "Akzeptiere alle Anfragen" + "Bist du sicher, dass du alle Beitrittsanfragen akzeptieren möchtest?" + "Akzeptiere alle Beitrittsanfragen" "Alle akzeptieren" - "Wir konnten nicht alle Anfragen annehmen. Möchten Sie es noch einmal versuchen?" - "Es konnten nicht alle Anfragen akzeptiert werden" + "Wir konnten nicht alle Beitrittsanfragen annehmen. Möchtest du es noch mal versuchen?" + "Es konnten nicht alle Beitrittsanfragen akzeptiert werden" "Alle Beitrittsanfragen werden angenommen" - "Wir konnten diese Anfrage nicht annehmen. Möchten Sie es noch einmal versuchen?" - "Die Anfrage konnte nicht akzeptiert werden" + "Wir konnten diese Beitrittsanfrage nicht annehmen. Möchtest du es noch mal versuchen?" + "Die Beitrittsanfrage 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 keine erneute Zulassung auf diesen Chatroom anfordern." - "Ablehnen und Zugriff verbieten" + "Bist du sicher, dass du %1$s ablehnen und sperren möchtest? Dieser Nutzer kann dann keinen erneuten Beitritt zu diesem Chat anfragen." + "Ablehnen und Zugriff sperren" "Ablehnung und Sperrung des Zugriffs" "Ja, ablehnen" - "Sind Sie sicher, dass Sie die %1$s Anfrage, diesem Chatroom beizutreten, ablehnen möchten ?" - "Zugriff verweigern" + "Bist du sicher, dass du die Beitrittsanfrage von %1$s zu diesem Chat ablehnen möchtest?" + "Zugriff ablehnen" "Ablehnen und sperren" - "Wir konnten diese Anfrage nicht ablehnen. Möchten Sie es noch einmal versuchen?" - "Anfrage konnte nicht abgelehnt werden" + "Wir konnten diese Beitrittsanfrage nicht ablehnen. Möchtest du es noch mal versuchen?" + "Beitrittsanfrage konnte nicht abgelehnt werden" "Ablehnung der Beitrittsanfrage" - "Falls jemand um Aufnahme in den Raum bittet, können Sie dessen Anfrage hier sehen." + "Sollte jemand um Beitritt zum Chat bitten, kannst du die Anfrage hier sehen." "Keine ausstehende Beitrittsanfrage" "Beitrittsanfragen werden geladen …" "Beitrittsanfragen" - "%1$s+ %2$d andere wollen diesem Chatroom beitreten" - "%1$s+ %2$d andere wollen diesem Chatroom beitreten" + "%1$s +%2$d weiterer möchten diesem Chat beitreten" + "%1$s +%2$d weitere möchten diesem Chat beitreten" "Alles ansehen" "Akzeptieren" - "%1$s möchte diesem Chatroom beitreten" + "%1$s möchte diesem Chat beitreten" "Ansicht" diff --git a/features/knockrequests/impl/src/main/res/values-ko/translations.xml b/features/knockrequests/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..ef97ab4a1e --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,35 @@ + + + "네, 모두 수락합니다" + "모든 가입 요청을 정말로 수락하시겠습니까?" + "모든 요청 수락" + "모두 수락" + "모든 요청을 처리할 수 없습니다. 다시 시도하시겠습니까?" + "모든 요청을 수락하지 못했습니다." + "모든 가입 요청 수락" + "이 요청을 수락할 수 없습니다. 다시 시도하시겠습니까?" + "요청을 수락하지 못했습니다" + "가입 요청 수락" + "네, 거절하고 차단합니다" + "%1$s 을 거부하고 차단하시겠습니까? 이 사용자는 이 방에 다시 참여하기 위해 액세스를 요청할 수 없습니다." + "접근 거부 및 차단" + "접근 거부 및 차단" + "네, 거절합니다" + "%1$s 의 이 방에 대한 요청을 정말 거부하시겠습니까?" + "접근 거부" + "거부 및 차단" + "이 요청을 거부할 수 없습니다. 다시 시도하시겠습니까?" + "요청 거부에 실패했습니다" + "가입 요청 거부" + "누군가가 방에 참여 요청을 한다면, 여기에서 그 요청을 볼 수 있습니다." + "보류 중인 가입 요청이 없습니다." + "가입 요청을 로딩 중…" + "참여 요청" + + "%1$s +%2$d 명이 이 방에 참여하고 싶어합니다." + + "모두 보기" + "수락" + "%1$s 이 방에 참여하고 싶습니다." + "보기" + diff --git a/features/knockrequests/impl/src/main/res/values-pt-rBR/translations.xml b/features/knockrequests/impl/src/main/res/values-pt-rBR/translations.xml index b602cedaf5..4ccedac193 100644 --- a/features/knockrequests/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-pt-rBR/translations.xml @@ -1,33 +1,33 @@ "Sim, aceitar todos" - "Tem certeza de que deseja aceitar todos os pedidos de adesão?" + "Tem certeza de que deseja aceitar todos os pedidos de entrada?" "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" + "Aceitando todas as solicitações de entrada" "Não pudemos aceitar essa solicitação. Você gostaria de tentar novamente?" "Falha ao aceitar a solicitação" - "Aceitando solicitação de adesão" + "Aceitando solicitação de entrada" "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?" + "Você tem certeza de que deseja recusar a solicitação de %1$s para entrar nesta 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" + "Recusando a solicitação de entrada" "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" + "Nenhum pedido de entrada pendente" + "Carregando solicitações de entrada…" + "Pedidos de entrada" - "%1$s +%2$d outro desejam entrar desta sala" - "%1$s +%2$d outros desejam entrar desta sala" + "%1$s + outro %2$d desejam entrar nesta sala" + "%1$s + outros %2$d desejam entrar nesta sala" "Ver tudo" "Aceitar" diff --git a/features/knockrequests/impl/src/main/res/values-ro/translations.xml b/features/knockrequests/impl/src/main/res/values-ro/translations.xml index 2a75681c5d..06bfbc9674 100644 --- a/features/knockrequests/impl/src/main/res/values-ro/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-ro/translations.xml @@ -1,4 +1,37 @@ + "Da, acceptati tot" + "Sunteți sigur că doriți să acceptați toate cererile de alăturare?" + "Acceptați toate cererile" + "Acceptați tot" + "Nu am putut accepta toate cererile. Doriți să încercați din nou?" + "Nu s-au putut accepta toate cererile" + "Se acceptă toate cererile de alăturare" + "Nu am putut accepta această cerere. Doriți să încercați din nou?" + "Nu s-a putut accepta cererea" + "Se acceptă cererea de alăturare" + "Da, refuzați și interziceți" + "Sunteți sigur că doriți să refuzați și să interziceți accesul lui %1$s? Acest utilizator nu va mai putea cere accesul pentru a se alătura acestei camere." + "Refuzați și interziceți accesul" + "Se refuză și interzice accesul" + "Da, refuzați" + "Sunteți sigur că doriți să refuzați cererea %1$s de a vă alătura acestei camere?" + "Refuzați accesul" + "Refuzați și interziceți" + "Nu am putut refuza această cerere. Doriți să încercați din nou?" + "Cererea nu a putut fi respinsă" + "Se refuza cererea de alăturare" + "Când cineva va cere să se alăture camerei, veți putea vedea cererea aici." + "Nu există cereri de alăturare în așteptare" + "Se încarcă cererile de alăturare…" + "Cereri de alăturare" + + "%1$s +%2$d utilizator doresc să se alăture acestei camere" + "%1$s +%2$d utilizatori doresc să se alăture acestei camere" + "%1$s +%2$d utilizatori doresc să se alăture acestei camere" + + "Vizualizați tot" "Acceptați" + "%1$s dorește să se alăture acestei camere" + "Vizualizați" diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPointTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPointTest.kt new file mode 100644 index 0000000000..b6b6d766c7 --- /dev/null +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPointTest.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.features.knockrequests.impl.list + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.tests.testutils.node.TestParentNode +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class DefaultKnockRequestsListEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() = runTest { + val entryPoint = DefaultKnockRequestsListEntryPoint() + + val parentNode = TestParentNode.create { buildContext, plugins -> + KnockRequestsListNode( + buildContext = buildContext, + plugins = plugins, + presenter = createKnockRequestsListPresenter(), + ) + } + val result = entryPoint.createNode(parentNode, BuildContext.root(null)) + assertThat(result).isInstanceOf(KnockRequestsListNode::class.java) + } +} diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt index 2d33642a06..18269e06f3 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt @@ -286,19 +286,19 @@ class KnockRequestsListPresenterTest { assert(acceptFailureLambda).isCalledOnce() assert(acceptSuccessLambda).isCalledOnce() } - - private fun TestScope.createKnockRequestsListPresenter( - canAccept: Boolean = true, - canDecline: Boolean = true, - canBan: Boolean = true, - knockRequestsFlow: Flow> = flowOf(emptyList()) - ): KnockRequestsListPresenter { - val knockRequestsService = KnockRequestsService( - knockRequestsFlow = knockRequestsFlow, - coroutineScope = backgroundScope, - isKnockFeatureEnabledFlow = flowOf(true), - permissionsFlow = flowOf(KnockRequestPermissions(canAccept, canDecline, canBan)), - ) - return KnockRequestsListPresenter(knockRequestsService = knockRequestsService) - } +} + +internal fun TestScope.createKnockRequestsListPresenter( + canAccept: Boolean = true, + canDecline: Boolean = true, + canBan: Boolean = true, + knockRequestsFlow: Flow> = flowOf(emptyList()) +): KnockRequestsListPresenter { + val knockRequestsService = KnockRequestsService( + knockRequestsFlow = knockRequestsFlow, + coroutineScope = backgroundScope, + isKnockFeatureEnabledFlow = flowOf(true), + permissionsFlow = flowOf(KnockRequestPermissions(canAccept, canDecline, canBan)), + ) + return KnockRequestsListPresenter(knockRequestsService = knockRequestsService) } diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomRenderer.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomRenderer.kt index 8bd6fe830b..80252a83a6 100644 --- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomRenderer.kt +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomRenderer.kt @@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import io.element.android.libraries.matrix.api.core.RoomId -interface LeaveRoomRenderer { +fun interface LeaveRoomRenderer { @Composable fun Render( state: LeaveRoomState, diff --git a/features/leaveroom/api/src/main/res/values-bg/translations.xml b/features/leaveroom/api/src/main/res/values-bg/translations.xml index 69d203216a..8bb88631d5 100644 --- a/features/leaveroom/api/src/main/res/values-bg/translations.xml +++ b/features/leaveroom/api/src/main/res/values-bg/translations.xml @@ -1,4 +1,6 @@ + "Сигурни ли сте, че искате да напуснете тази стая? Вие сте единственият човек тук. Ако напуснете, никой няма да може да се присъедини в бъдеще, включително и вие." + "Сигурни ли сте, че искате да напуснете тази стая? Тази стая не е общодостъпна и няма да можете да се присъедините отново без покана." "Сигурни ли сте, че искате да напуснете стаята?" diff --git a/features/leaveroom/api/src/main/res/values-cs/translations.xml b/features/leaveroom/api/src/main/res/values-cs/translations.xml index d3163cfbed..3853553cf1 100644 --- a/features/leaveroom/api/src/main/res/values-cs/translations.xml +++ b/features/leaveroom/api/src/main/res/values-cs/translations.xml @@ -3,5 +3,7 @@ "Opravdu chcete opustit tuto konverzaci? Tato konverzace není veřejná a bez pozvánky se k ní nebudete moci znovu připojit." "Opravdu chcete opustit tuto místnost? Jste tu jediná osoba. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás." "Opravdu chcete opustit tuto místnost? Tato místnost není veřejná a bez pozvánky se nebudete moci znovu připojit." + "Vyberte vlastníky" + "Jste jediným vlastníkem této místnost. Než místnost opustíte, musíte vlastnictví převést na někoho jiného." "Opravdu chcete opustit místnost?" diff --git a/features/leaveroom/api/src/main/res/values-cy/translations.xml b/features/leaveroom/api/src/main/res/values-cy/translations.xml index 1ce4f2f3b4..ea02d4d869 100644 --- a/features/leaveroom/api/src/main/res/values-cy/translations.xml +++ b/features/leaveroom/api/src/main/res/values-cy/translations.xml @@ -3,5 +3,8 @@ "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." + "Dewiswch Berchnogion" + "Chi yw unig berchennog yr ystafell hon. Mae angen i chi drosglwyddo perchnogaeth i rywun arall cyn i chi adael yr room." + "Trosglwyddo perchnogaeth" "Ydych chi\'n siŵr eich bod am adael yr ystafell?" 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 9a9a53a769..2a0326d5ea 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,10 @@ - "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?" + "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 Chat 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 Chat verlassen möchtest? Dieser Chat ist nicht öffentlich und du kannst ihm ohne Einladung nicht erneut beitreten." + "Wähle Eigentümer" + "Du bist der einzige Eigentümer dieses Chats. Du musst die Eigentumsrechte an jemand anderen übertragen, bevor du den Chat verlässt." + "Eigentumsrechte übertragen" + "Bist du sicher, dass du den Chat verlassen willst?" diff --git a/features/leaveroom/api/src/main/res/values-it/translations.xml b/features/leaveroom/api/src/main/res/values-it/translations.xml index 1b403e8f93..3eb276166b 100644 --- a/features/leaveroom/api/src/main/res/values-it/translations.xml +++ b/features/leaveroom/api/src/main/res/values-it/translations.xml @@ -3,5 +3,8 @@ "Vuoi davvero abbandonare questa conversazione? La conversazione non è pubblica e non potrai rientrare senza un invito." "Sei sicuro di voler lasciare questa stanza? Sei l\'unica persona presente. Se esci, nessuno potrà unirsi in futuro, te compreso." "Sei sicuro di voler lasciare questa stanza? Questa stanza non è pubblica e non potrai rientrare senza un invito." + "Scegli i proprietari" + "Sei l\'unico proprietario di questa stanza. Devi trasferire la proprietà a qualcun altro prima di lasciare la stanza." + "Trasferisci proprietà" "Sei sicuro di voler lasciare la stanza?" diff --git a/features/leaveroom/api/src/main/res/values-ko/translations.xml b/features/leaveroom/api/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..71f5bbe47e --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-ko/translations.xml @@ -0,0 +1,10 @@ + + + "이 대화를 나가시겠습니까? 이 대화는 공개되지 않았으므로 초대 없이는 다시 참여할 수 없습니다." + "정말로 이 방을 떠나시겠어요? 이 방에서 유일하게 남은 사용자입니다. 나간 이후부터는 당신을 포함해서 아무도 다시 참여할 수 없어요." + "정말로 이 방을 떠나시겠어요? 이 방은 공개가 아니기 때문에 초대 없이는 다시 참여할 수 없습니다." + "소유자 선택" + "이 방의 유일한 소유자는 귀하입니다. 방을 떠나기 전에 다른 사람에게 소유권을 양도해야 합니다." + "소유권 이전" + "정말로 이 방을 떠나시겠어요?" + 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 1e1800e0d0..7286ca93cf 100644 --- a/features/leaveroom/api/src/main/res/values-nb/translations.xml +++ b/features/leaveroom/api/src/main/res/values-nb/translations.xml @@ -3,5 +3,8 @@ "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." + "Velg eiere" + "Du er den eneste eieren av dette rommet. Du må overføre eierskapet til noen andre før du forlater rommet." + "Overfør eierskap" "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 b55010ad80..178e43bef6 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,7 +1,10 @@ "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? Você é a única pessoa aqui. Se você sair, ninguém poderá entrar no futuro, até mesmo 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." + "Escolher proprietários" + "Você é o único proprietário desta sala. Você precisa transferir a posse para outra pessoa antes de sair da sala." + "Transferir posse" "Tem certeza de que deseja sair da sala?" diff --git a/features/leaveroom/api/src/main/res/values-ro/translations.xml b/features/leaveroom/api/src/main/res/values-ro/translations.xml index 5af1590439..8f504148f6 100644 --- a/features/leaveroom/api/src/main/res/values-ro/translations.xml +++ b/features/leaveroom/api/src/main/res/values-ro/translations.xml @@ -3,5 +3,8 @@ "Sunteți sigur că doriți să părăsiți această conversație? Această conversație nu este publică și nu veți putea reveni fără o invitație." "Sunteți sigur că vreți să părăsiți această cameră? Sunteți singura persoană de aici. Dacă o părasiți, nimeni nu se va mai putea alătura în viitor, inclusiv dumneavoastra." "Sunteți sigur că vrei să părăsiți această cameră? Această cameră nu este publică și nu va veti putea alătura din nou fără o invitație." + "Alegeți proprietari" + "Sunteți singurul proprietar al acestei camere. Trebuie să transferați proprietatea către o altă persoană înainte de a părăsi camera." + "Transferați proprietatea" "Sunteți sigur că vreți să părăsiți camera?" diff --git a/features/leaveroom/api/src/main/res/values-ru/translations.xml b/features/leaveroom/api/src/main/res/values-ru/translations.xml index ebf7ac82cd..907b729610 100644 --- a/features/leaveroom/api/src/main/res/values-ru/translations.xml +++ b/features/leaveroom/api/src/main/res/values-ru/translations.xml @@ -3,5 +3,8 @@ "Вы уверены, что хотите покинуть беседу? Эта беседа не является общедоступной, и Вы не сможете присоединиться к ней без приглашения." "Вы уверены, что хотите покинуть эту комнату? Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас." "Вы уверены, что хотите покинуть эту комнату? Эта комната не является общедоступной, и Вы не сможете присоединиться к ней без приглашения." + "Назначить владельцев" + "Вы единственный владелец этой комнаты. Перед тем, как её покинуть, необходимо передать владение кому-нибудь другому." + "Передача владения" "Вы уверены, что хотите покинуть комнату?" diff --git a/features/leaveroom/api/src/main/res/values-sv/translations.xml b/features/leaveroom/api/src/main/res/values-sv/translations.xml index c80d716329..2d26a6ec32 100644 --- a/features/leaveroom/api/src/main/res/values-sv/translations.xml +++ b/features/leaveroom/api/src/main/res/values-sv/translations.xml @@ -3,5 +3,8 @@ "Är du säker på att du vill lämna den här konversationen? Den här konversationen är inte offentlig och du kommer inte att kunna gå med igen utan en inbjudan." "Är du säker på att du vill lämna det här rummet? Du är den enda personen här. Om du lämnar kommer ingen att kunna gå med i framtiden, inklusive du." "Är du säker på att du vill lämna det här rummet? Detta rum är inte offentligt och du kommer inte att kunna gå med igen utan en inbjudan." + "Välj ägare" + "Du är den enda ägaren av det här rummet. Du måste överföra ägarskapet till någon annan innan du lämnar rummet." + "Överför ägarskap" "Är du säker på att du vill lämna rummet?" diff --git a/features/leaveroom/api/src/main/res/values-uz/translations.xml b/features/leaveroom/api/src/main/res/values-uz/translations.xml index 59c111e2ac..374fd347fc 100644 --- a/features/leaveroom/api/src/main/res/values-uz/translations.xml +++ b/features/leaveroom/api/src/main/res/values-uz/translations.xml @@ -1,5 +1,6 @@ + "Bu suhbatni tark etmoqchi ekanligingizga ishonchingiz komilmi? Bu suhbat hammaga ochiq emas va siz taklifsiz qayta qo‘shila olmaysiz." "Bu xonani tark etmoqchi ekanligingizga ishonchingiz komilmi? Siz bu yerda yagona odamsiz. Agar siz tark etsangiz, kelajakda hech kim qo\'shila olmaydi, jumladan siz ham." "Bu xonani tark etmoqchi ekanligingizga ishonchingiz komilmi? Bu xona ochiq emas va siz taklifsiz qayta qo‘shila olmaysiz." "Xonani tark etmoqchi ekanligingizga ishonchingiz komilmi?" diff --git a/features/leaveroom/api/src/main/res/values-zh/translations.xml b/features/leaveroom/api/src/main/res/values-zh/translations.xml index e9dcf44fd7..6b7f17558b 100644 --- a/features/leaveroom/api/src/main/res/values-zh/translations.xml +++ b/features/leaveroom/api/src/main/res/values-zh/translations.xml @@ -3,5 +3,8 @@ "您确定要离开此对话吗?此对话不公开,未经邀请您将无法重新加入。" "确定要离开此聊天室吗?此处只有你一个人。如果离开此聊天室,包括你在内的所有人都将无法进入。" "确定要离开此聊天室吗?此聊天室不公开,没有邀请你将无法重新加入。" + "选择所有者" + "您是本房间的唯一所有者。离开房间前,您需要将所有权转移给他人。" + "转让所有权" "确定要离开聊天室吗?" diff --git a/features/leaveroom/impl/build.gradle.kts b/features/leaveroom/impl/build.gradle.kts index ff9eb5de95..81aedb2025 100644 --- a/features/leaveroom/impl/build.gradle.kts +++ b/features/leaveroom/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -15,7 +16,7 @@ android { namespace = "io.element.android.features.leaveroom.impl" } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.leaveroom.api) @@ -26,13 +27,8 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.libraries.push.api) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) + testCommonDependencies(libs) testImplementation(libs.coroutines.core) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.push.test) - testImplementation(projects.tests.testutils) } diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomRenderer.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomRenderer.kt index 254a3af1ad..be1aa3b55a 100644 --- a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomRenderer.kt +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomRenderer.kt @@ -9,15 +9,16 @@ package io.element.android.features.leaveroom.impl import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.leaveroom.api.LeaveRoomRenderer import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class InternalLeaveRoomRenderer @Inject constructor() : LeaveRoomRenderer { +@Inject +class InternalLeaveRoomRenderer : LeaveRoomRenderer { @Composable override fun Render(state: LeaveRoomState, onSelectNewOwners: (RoomId) -> Unit, modifier: Modifier) { if (state is InternalLeaveRoomState) { 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 d1575aac91..50242ab7ea 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 @@ -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 dev.zacsweers.metro.Inject import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.libraries.architecture.AsyncAction @@ -29,9 +30,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject -class LeaveRoomPresenter @Inject constructor( +@Inject +class LeaveRoomPresenter( private val client: MatrixClient, private val dispatchers: CoroutineDispatchers, private val notificationConversationService: NotificationConversationService, diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomModule.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomModule.kt index 840c7a9eec..b13d7d3078 100644 --- a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomModule.kt +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomModule.kt @@ -7,16 +7,16 @@ package io.element.android.features.leaveroom.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.impl.LeaveRoomPresenter import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.SessionScope @ContributesTo(SessionScope::class) -@Module +@BindingContainer interface LeaveRoomModule { @Binds fun bindLeaveRoomPresenter(presenter: LeaveRoomPresenter): Presenter diff --git a/features/licenses/api/src/main/kotlin/io/element/android/features/licenses/api/OpenSourceLicensesEntryPoint.kt b/features/licenses/api/src/main/kotlin/io/element/android/features/licenses/api/OpenSourceLicensesEntryPoint.kt index e4beebe6ef..a2dbdde60d 100644 --- a/features/licenses/api/src/main/kotlin/io/element/android/features/licenses/api/OpenSourceLicensesEntryPoint.kt +++ b/features/licenses/api/src/main/kotlin/io/element/android/features/licenses/api/OpenSourceLicensesEntryPoint.kt @@ -7,9 +7,6 @@ package io.element.android.features.licenses.api -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.SimpleFeatureEntryPoint -interface OpenSourceLicensesEntryPoint { - fun getNode(node: Node, buildContext: BuildContext): Node -} +interface OpenSourceLicensesEntryPoint : SimpleFeatureEntryPoint diff --git a/features/licenses/impl/build.gradle.kts b/features/licenses/impl/build.gradle.kts index 52589aeaf5..59ad326b6f 100644 --- a/features/licenses/impl/build.gradle.kts +++ b/features/licenses/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -17,7 +18,7 @@ android { namespace = "io.element.android.features.licenses.impl" } -setupAnvil() +setupDependencyInjection() dependencies { implementation(libs.serialization.json) @@ -26,12 +27,8 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.uiStrings) api(projects.features.licenses.api) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) + + testCommonDependencies(libs) testImplementation(libs.coroutines.core) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.tests.testutils) } diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPoint.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPoint.kt index 170e819ce7..8ffbc05ed3 100644 --- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPoint.kt +++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPoint.kt @@ -9,15 +9,16 @@ package io.element.android.features.licenses.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultOpenSourcesLicensesEntryPoint @Inject constructor() : OpenSourceLicensesEntryPoint { - override fun getNode(node: Node, buildContext: BuildContext): Node { - return node.createNode(buildContext) +@Inject +class DefaultOpenSourcesLicensesEntryPoint : OpenSourceLicensesEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext): Node { + return parentNode.createNode(buildContext) } } diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DependenciesFlowNode.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DependenciesFlowNode.kt index 91cab27de9..30e094e38e 100644 --- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DependenciesFlowNode.kt +++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DependenciesFlowNode.kt @@ -15,20 +15,21 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.licenses.impl.details.DependenciesDetailsNode import io.element.android.features.licenses.impl.list.DependencyLicensesListNode import io.element.android.features.licenses.impl.model.DependencyLicenseItem import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope import kotlinx.parcelize.Parcelize @ContributesNode(AppScope::class) -class DependenciesFlowNode @AssistedInject constructor( +@AssistedInject +class DependenciesFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, ) : BaseFlowNode( diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/LicensesProvider.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/LicensesProvider.kt index 4c4db861e9..c5c0fa84f5 100644 --- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/LicensesProvider.kt +++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/LicensesProvider.kt @@ -8,23 +8,24 @@ package io.element.android.features.licenses.impl import android.content.Context -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.licenses.impl.model.DependencyLicenseItem import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.ApplicationContext import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream -import javax.inject.Inject interface LicensesProvider { suspend fun provides(): List } @ContributesBinding(AppScope::class) -class AssetLicensesProvider @Inject constructor( +@Inject +class AssetLicensesProvider( @ApplicationContext private val context: Context, private val dispatchers: CoroutineDispatchers, ) : LicensesProvider { diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsNode.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsNode.kt index ae9ad03e9d..4f55f8dfa8 100644 --- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsNode.kt +++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsNode.kt @@ -12,16 +12,17 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.licenses.impl.model.DependencyLicenseItem import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class DependenciesDetailsNode @AssistedInject constructor( +@AssistedInject +class DependenciesDetailsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, ) : Node( diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListNode.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListNode.kt index 039ac24b20..7280c2ad41 100644 --- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListNode.kt +++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListNode.kt @@ -13,14 +13,15 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.licenses.impl.model.DependencyLicenseItem -import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class DependencyLicensesListNode @AssistedInject constructor( +@AssistedInject +class DependencyLicensesListNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: DependencyLicensesListPresenter, 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 a0d3a19ed2..7c36a4f1ef 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 @@ -13,6 +13,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.licenses.impl.LicensesProvider import io.element.android.features.licenses.impl.model.DependencyLicenseItem import io.element.android.libraries.architecture.AsyncData @@ -20,9 +21,9 @@ 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 -class DependencyLicensesListPresenter @Inject constructor( +@Inject +class DependencyLicensesListPresenter( private val licensesProvider: LicensesProvider, ) : Presenter { @Composable diff --git a/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPointTest.kt b/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPointTest.kt new file mode 100644 index 0000000000..3209e28fe6 --- /dev/null +++ b/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPointTest.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.licenses.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultOpenSourcesLicensesEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultOpenSourcesLicensesEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + DependenciesFlowNode( + buildContext = buildContext, + plugins = plugins, + ) + } + val result = entryPoint.createNode(parentNode, BuildContext.root(null)) + assertThat(result).isInstanceOf(DependenciesFlowNode::class.java) + } +} diff --git a/features/location/api/build.gradle.kts b/features/location/api/build.gradle.kts index 4ce33a748a..e4d5c4e886 100644 --- a/features/location/api/build.gradle.kts +++ b/features/location/api/build.gradle.kts @@ -8,6 +8,7 @@ import config.BuildTimeConfig import extension.buildConfigFieldStr import extension.readLocalProperty +import extension.testCommonDependencies plugins { id("io.element.android-compose-library") @@ -70,6 +71,5 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(libs.coil.compose) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) + testCommonDependencies(libs) } diff --git a/features/location/impl/build.gradle.kts b/features/location/impl/build.gradle.kts index 4c62bdff1a..43f6bba0d0 100644 --- a/features/location/impl/build.gradle.kts +++ b/features/location/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -20,7 +21,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.location.api) @@ -37,19 +38,10 @@ dependencies { implementation(projects.services.analytics.api) implementation(libs.accompanist.permission) implementation(projects.libraries.uiStrings) - implementation(libs.dagger) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.testtags) testImplementation(projects.services.analytics.test) testImplementation(projects.features.messages.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } 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 index e662ef8115..5be8e7c093 100644 --- 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 @@ -7,14 +7,15 @@ package io.element.android.features.location.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 { +@Inject +class DefaultLocationService : 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 21cc8df7bd..c879635052 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 @@ -12,18 +12,19 @@ import android.content.Intent import android.net.Uri import androidx.annotation.VisibleForTesting import androidx.core.net.toUri -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 io.element.android.libraries.di.annotations.ApplicationContext import timber.log.Timber import java.util.Locale -import javax.inject.Inject @ContributesBinding(AppScope::class) -class AndroidLocationActions @Inject constructor( +@Inject +class AndroidLocationActions( @ApplicationContext private val context: Context ) : LocationActions { override fun share(location: Location, label: String?) { diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt index fe6c24f6a7..0ef520aadb 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt @@ -11,14 +11,15 @@ import androidx.compose.runtime.Composable import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberMultiplePermissionsState -import com.squareup.anvil.annotations.ContributesBinding -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesBinding @Suppress("unused") -class DefaultPermissionsPresenter @AssistedInject constructor( +@AssistedInject +class DefaultPermissionsPresenter( @Assisted private val permissions: List ) : PermissionsPresenter { @AssistedFactory diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsPresenter.kt index 410214ec88..82e54ba977 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsPresenter.kt @@ -10,7 +10,7 @@ package io.element.android.features.location.impl.common.permissions import io.element.android.libraries.architecture.Presenter interface PermissionsPresenter : Presenter { - interface Factory { + fun interface Factory { fun create(permissions: List): PermissionsPresenter } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPoint.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPoint.kt index cf601a412e..c42be68bf1 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPoint.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPoint.kt @@ -9,15 +9,16 @@ package io.element.android.features.location.impl.send import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.location.api.SendLocationEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.timeline.Timeline -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultSendLocationEntryPoint @Inject constructor() : SendLocationEntryPoint { +@Inject +class DefaultSendLocationEntryPoint : SendLocationEntryPoint { override fun builder(timelineMode: Timeline.Mode): SendLocationEntryPoint.Builder { return Builder(timelineMode) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationNode.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationNode.kt index 97e78fcb07..8fff96e7b6 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationNode.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationNode.kt @@ -13,10 +13,10 @@ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope @@ -24,7 +24,8 @@ import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(RoomScope::class) -class SendLocationNode @AssistedInject constructor( +@AssistedInject +class SendLocationNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: SendLocationPresenter.Factory, 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 2619352af1..9914920ac3 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 @@ -15,9 +15,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.Composer import io.element.android.features.location.impl.common.MapDefaults import io.element.android.features.location.impl.common.actions.LocationActions @@ -36,7 +36,8 @@ import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.launch -class SendLocationPresenter @AssistedInject constructor( +@AssistedInject +class SendLocationPresenter( permissionsPresenterFactory: PermissionsPresenter.Factory, private val room: JoinedRoom, @Assisted private val timelineMode: Timeline.Mode, @@ -46,7 +47,7 @@ class SendLocationPresenter @AssistedInject constructor( private val buildMeta: BuildMeta, ) : Presenter { @AssistedFactory - interface Factory { + fun interface Factory { fun create(timelineMode: Timeline.Mode): SendLocationPresenter } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPoint.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPoint.kt index ea45ef2690..d226a01ede 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPoint.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPoint.kt @@ -9,14 +9,15 @@ package io.element.android.features.location.impl.show import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.location.api.ShowLocationEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultShowLocationEntryPoint @Inject constructor() : ShowLocationEntryPoint { +@Inject +class DefaultShowLocationEntryPoint : ShowLocationEntryPoint { override fun createNode(parentNode: Node, buildContext: BuildContext, inputs: ShowLocationEntryPoint.Inputs): Node { return parentNode.createNode(buildContext, listOf(inputs)) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationNode.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationNode.kt index 1a74130eb0..d5977fa426 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationNode.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationNode.kt @@ -13,21 +13,22 @@ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.features.location.api.ShowLocationEntryPoint import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(RoomScope::class) -class ShowLocationNode @AssistedInject constructor( - presenterFactory: ShowLocationPresenter.Factory, - analyticsService: AnalyticsService, +@AssistedInject +class ShowLocationNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, + presenterFactory: ShowLocationPresenter.Factory, + analyticsService: AnalyticsService, ) : Node(buildContext, plugins = plugins) { init { lifecycle.subscribe( diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt index 152a201ad2..7929843571 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt @@ -14,9 +14,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.location.api.Location import io.element.android.features.location.impl.common.MapDefaults import io.element.android.features.location.impl.common.actions.LocationActions @@ -26,15 +26,16 @@ import io.element.android.features.location.impl.common.permissions.PermissionsS import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta -class ShowLocationPresenter @AssistedInject constructor( +@AssistedInject +class ShowLocationPresenter( + @Assisted private val location: Location, + @Assisted private val description: String?, permissionsPresenterFactory: PermissionsPresenter.Factory, private val locationActions: LocationActions, private val buildMeta: BuildMeta, - @Assisted private val location: Location, - @Assisted private val description: String? ) : Presenter { @AssistedFactory - interface Factory { + fun interface Factory { fun create(location: Location, description: String?): ShowLocationPresenter } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPointTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPointTest.kt new file mode 100644 index 0000000000..9be79c7092 --- /dev/null +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPointTest.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. + */ + +package io.element.android.features.location.impl.send + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.features.location.impl.common.actions.FakeLocationActions +import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter +import io.element.android.features.messages.test.FakeMessageComposerContext +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultSendLocationEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultSendLocationEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + SendLocationNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { timelineMode: Timeline.Mode -> + SendLocationPresenter( + permissionsPresenterFactory = { FakePermissionsPresenter() }, + room = FakeJoinedRoom(), + timelineMode = timelineMode, + analyticsService = FakeAnalyticsService(), + messageComposerContext = FakeMessageComposerContext(), + locationActions = FakeLocationActions(), + buildMeta = aBuildMeta(), + ) + }, + analyticsService = FakeAnalyticsService(), + ) + } + val timelineMode = Timeline.Mode.Live + val result = entryPoint.builder(timelineMode) + .build(parentNode, BuildContext.root(null)) + assertThat(result).isInstanceOf(SendLocationNode::class.java) + assertThat(result.plugins).contains(SendLocationNode.Inputs(timelineMode)) + } +} diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt new file mode 100644 index 0000000000..d31ef0c0e4 --- /dev/null +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.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.location.impl.show + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.features.location.api.Location +import io.element.android.features.location.api.ShowLocationEntryPoint +import io.element.android.features.location.impl.common.actions.FakeLocationActions +import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultShowLocationEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultShowLocationEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + ShowLocationNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { location: Location, description: String? -> + ShowLocationPresenter( + permissionsPresenterFactory = { FakePermissionsPresenter() }, + locationActions = FakeLocationActions(), + buildMeta = aBuildMeta(), + location = location, + description = description, + ) + }, + analyticsService = FakeAnalyticsService(), + ) + } + val inputs = ShowLocationEntryPoint.Inputs( + location = Location(37.4219983, -122.084, 10f), + description = "My location", + ) + val result = entryPoint.createNode( + parentNode, + BuildContext.root(null), + inputs = inputs, + ) + assertThat(result).isInstanceOf(ShowLocationNode::class.java) + assertThat(result.plugins).contains(inputs) + } +} diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt index 937d1d475e..cc53badbb2 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt @@ -37,10 +37,10 @@ class ShowLocationPresenterTest { permissionsPresenterFactory = object : PermissionsPresenter.Factory { override fun create(permissions: List): PermissionsPresenter = fakePermissionsPresenter }, - fakeLocationActions, - fakeBuildMeta, - location, - A_DESCRIPTION, + locationActions = fakeLocationActions, + buildMeta = fakeBuildMeta, + location = location, + description = A_DESCRIPTION, ) @Test 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 index 8ba29a65aa..69a3331ffb 100644 --- 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 @@ -10,7 +10,7 @@ package io.element.android.features.location.test import io.element.android.features.location.api.LocationService class FakeLocationService( - private val isServiceAvailable: Boolean, + private val isServiceAvailable: Boolean = false, ) : LocationService { override fun isServiceAvailable() = isServiceAvailable } diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index ad77b20e60..998b763804 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -20,13 +21,14 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.lockscreen.api) implementation(projects.appconfig) implementation(projects.features.enterprise.api) implementation(projects.libraries.core) + implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) @@ -37,27 +39,19 @@ dependencies { implementation(projects.libraries.testtags) implementation(projects.libraries.uiUtils) implementation(projects.features.logout.api) + implementation(projects.libraries.uiCommon) implementation(projects.libraries.uiStrings) implementation(projects.libraries.sessionStorage.api) implementation(projects.services.appnavstate.api) implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.biometric) - 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) - testImplementation(libs.androidx.test.ext.junit) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.tests.testutils) testImplementation(projects.libraries.cryptography.test) testImplementation(projects.libraries.cryptography.impl) testImplementation(projects.libraries.featureflag.test) 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/DefaultLockScreenEntryPoint.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt index 907ed9fa84..1155a6fdcd 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt @@ -11,15 +11,16 @@ import android.content.Context import android.content.Intent import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint { +@Inject +class DefaultLockScreenEntryPoint : LockScreenEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder { val callbacks = mutableListOf() 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 8be72a113e..c1a97dadad 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 @@ -7,7 +7,10 @@ package io.element.android.features.lockscreen.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.lockscreen.api.LockScreenLockState import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager @@ -15,8 +18,6 @@ import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnl import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback 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.sessionstorage.api.observer.SessionListener import io.element.android.libraries.sessionstorage.api.observer.SessionObserver @@ -30,12 +31,12 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject import kotlin.time.Duration @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultLockScreenService @Inject constructor( +@Inject +class DefaultLockScreenService( private val lockScreenConfig: LockScreenConfig, private val lockScreenStore: LockScreenStore, private val pinCodeManager: PinCodeManager, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenConfig.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenConfig.kt index de20b6f09a..923eb32c76 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenConfig.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenConfig.kt @@ -7,10 +7,10 @@ package io.element.android.features.lockscreen.impl -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides import kotlin.time.Duration import io.element.android.appconfig.LockScreenConfig as AppConfigLockScreenConfig @@ -25,7 +25,7 @@ data class LockScreenConfig( ) @ContributesTo(AppScope::class) -@Module +@BindingContainer object LockScreenConfigModule { @Provides fun providesLockScreenConfig(): LockScreenConfig = LockScreenConfig( diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt index 56eaeb5472..94e2556ea2 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt @@ -15,9 +15,9 @@ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode import io.element.android.features.lockscreen.impl.setup.LockScreenSetupFlowNode @@ -29,7 +29,8 @@ import io.element.android.libraries.di.SessionScope import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -class LockScreenFlowNode @AssistedInject constructor( +@AssistedInject +class LockScreenFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, ) : BaseFlowNode( 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 a817faf353..b6b3c4115f 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 @@ -22,27 +22,28 @@ import androidx.compose.ui.res.stringResource import androidx.core.content.getSystemService import androidx.fragment.app.FragmentActivity import androidx.lifecycle.compose.LocalLifecycleOwner -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.lockscreen.impl.LockScreenConfig import io.element.android.features.lockscreen.impl.R import io.element.android.features.lockscreen.impl.storage.LockScreenStore import io.element.android.libraries.cryptography.api.EncryptionDecryptionService 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.di.annotations.ApplicationContext import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.util.concurrent.CopyOnWriteArrayList -import javax.inject.Inject private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_BIOMETRIC" @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) -class DefaultBiometricAuthenticatorManager @Inject constructor( +@Inject +class DefaultBiometricAuthenticatorManager( @ApplicationContext private val context: Context, private val lockScreenStore: LockScreenStore, private val lockScreenConfig: LockScreenConfig, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index 6dfd1e3e23..824db5339e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -7,22 +7,23 @@ package io.element.android.features.lockscreen.impl.pin -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.lockscreen.impl.storage.LockScreenStore import io.element.android.libraries.cryptography.api.EncryptionDecryptionService import io.element.android.libraries.cryptography.api.EncryptionResult import io.element.android.libraries.cryptography.api.SecretKeyRepository -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn import kotlinx.coroutines.flow.Flow import java.util.concurrent.CopyOnWriteArrayList -import javax.inject.Inject private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_PIN_CODE" @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) -class DefaultPinCodeManager @Inject constructor( +@Inject +class DefaultPinCodeManager( private val secretKeyRepository: SecretKeyRepository, private val encryptionDecryptionService: EncryptionDecryptionService, private val lockScreenStore: LockScreenStore, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt index 79a8c997c4..01aeaabe89 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt @@ -14,14 +14,13 @@ 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.node.node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.newRoot import com.bumble.appyx.navmodel.backstack.operation.push -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.setup.pin.SetupPinNode @@ -30,18 +29,20 @@ import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.ui.common.nodes.emptyNode import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -class LockScreenSettingsFlowNode @AssistedInject constructor( +@AssistedInject +class LockScreenSettingsFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val pinCodeManager: PinCodeManager, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.Unknown, + initialElement = NavTarget.Loading, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -49,7 +50,7 @@ class LockScreenSettingsFlowNode @AssistedInject constructor( ) { sealed interface NavTarget : Parcelable { @Parcelize - data object Unknown : NavTarget + data object Loading : NavTarget @Parcelize data object Unlock : NavTarget @@ -93,6 +94,9 @@ class LockScreenSettingsFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { + NavTarget.Loading -> { + emptyNode(buildContext) + } NavTarget.Unlock -> { val callback = object : PinUnlockNode.Callback { override fun onUnlock() { @@ -112,7 +116,6 @@ class LockScreenSettingsFlowNode @AssistedInject constructor( } createNode(buildContext, plugins = listOf(callback)) } - NavTarget.Unknown -> node(buildContext) { } } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt index 937927bb74..5e27b815bc 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt @@ -13,13 +13,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class LockScreenSettingsNode @AssistedInject constructor( +@AssistedInject +class LockScreenSettingsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: LockScreenSettingsPresenter, 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 9f44b03a10..6f238c964b 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 @@ -14,6 +14,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.lockscreen.impl.LockScreenConfig import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager @@ -23,9 +24,9 @@ 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 -class LockScreenSettingsPresenter @Inject constructor( +@Inject +class LockScreenSettingsPresenter( private val lockScreenConfig: LockScreenConfig, private val pinCodeManager: PinCodeManager, private val lockScreenStore: LockScreenStore, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/LockScreenSetupFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/LockScreenSetupFlowNode.kt index d6df41820f..263cc8ea54 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/LockScreenSetupFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/LockScreenSetupFlowNode.kt @@ -17,9 +17,9 @@ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback import io.element.android.features.lockscreen.impl.pin.PinCodeManager @@ -32,7 +32,8 @@ import io.element.android.libraries.di.SessionScope import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -class LockScreenSetupFlowNode @AssistedInject constructor( +@AssistedInject +class LockScreenSetupFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val pinCodeManager: PinCodeManager, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricNode.kt index eaa2189cc0..64da1a321c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricNode.kt @@ -14,13 +14,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class SetupBiometricNode @AssistedInject constructor( +@AssistedInject +class SetupBiometricNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: SetupBiometricPresenter, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt index d10485112d..f1183d8541 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt @@ -13,14 +13,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager import io.element.android.features.lockscreen.impl.storage.LockScreenStore import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.launch -import javax.inject.Inject -class SetupBiometricPresenter @Inject constructor( +@Inject +class SetupBiometricPresenter( private val lockScreenStore: LockScreenStore, private val biometricAuthenticatorManager: BiometricAuthenticatorManager, ) : Presenter { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinNode.kt index 5a4309c7be..aceb955b88 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinNode.kt @@ -12,13 +12,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class SetupPinNode @AssistedInject constructor( +@AssistedInject +class SetupPinNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: SetupPinPresenter, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt index b280c00957..36065484c7 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.lockscreen.impl.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.model.PinEntry @@ -21,14 +22,14 @@ import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPin import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta import kotlinx.coroutines.delay -import javax.inject.Inject /** * Some time for the ui to refresh before showing confirmation step. */ private const val DELAY_BEFORE_CONFIRMATION_STEP_IN_MILLIS = 100L -class SetupPinPresenter @Inject constructor( +@Inject +class SetupPinPresenter( private val lockScreenConfig: LockScreenConfig, private val pinValidator: PinValidator, private val buildMeta: BuildMeta, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/PinValidator.kt index 9716af0ac6..a12872fab1 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/PinValidator.kt @@ -7,11 +7,12 @@ package io.element.android.features.lockscreen.impl.setup.pin.validation +import dev.zacsweers.metro.Inject import io.element.android.features.lockscreen.impl.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.model.PinEntry -import javax.inject.Inject -class PinValidator @Inject constructor(private val lockScreenConfig: LockScreenConfig) { +@Inject +class PinValidator(private val lockScreenConfig: LockScreenConfig) { sealed interface Result { data object Valid : Result data class Invalid(val failure: SetupPinFailure) : Result diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt index b3086ddb9e..dbe51d3222 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt @@ -7,44 +7,40 @@ package io.element.android.features.lockscreen.impl.storage -import android.content.Context -import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.lockscreen.impl.LockScreenConfig -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.preferences.api.store.PreferenceDataStoreFactory import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import javax.inject.Inject -private val Context.dataStore: DataStore by preferencesDataStore(name = "pin_code_store") - -@SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class PreferencesLockScreenStore @Inject constructor( - @ApplicationContext private val context: Context, +@Inject +class PreferencesLockScreenStore( + preferenceDataStoreFactory: PreferenceDataStoreFactory, private val lockScreenConfig: LockScreenConfig, ) : LockScreenStore { + private val dataStore = preferenceDataStoreFactory.create("pin_code_store") + private val pinCodeKey = stringPreferencesKey("encoded_pin_code") private val remainingAttemptsKey = intPreferencesKey("remaining_pin_code_attempts") private val biometricUnlockKey = booleanPreferencesKey("biometric_unlock_enabled") override suspend fun getRemainingPinCodeAttemptsNumber(): Int { - return context.dataStore.data.map { preferences -> + return dataStore.data.map { preferences -> preferences.getRemainingPinCodeAttemptsNumber() }.first() } override suspend fun onWrongPin() { - context.dataStore.edit { preferences -> + dataStore.edit { preferences -> val current = preferences.getRemainingPinCodeAttemptsNumber() val remaining = (current - 1).coerceAtLeast(0) preferences[remainingAttemptsKey] = remaining @@ -52,43 +48,43 @@ class PreferencesLockScreenStore @Inject constructor( } override suspend fun resetCounter() { - context.dataStore.edit { preferences -> + dataStore.edit { preferences -> preferences[remainingAttemptsKey] = lockScreenConfig.maxPinCodeAttemptsBeforeLogout } } override suspend fun getEncryptedCode(): String? { - return context.dataStore.data.map { preferences -> + return dataStore.data.map { preferences -> preferences[pinCodeKey] }.first() } override suspend fun saveEncryptedPinCode(pinCode: String) { - context.dataStore.edit { preferences -> + dataStore.edit { preferences -> preferences[pinCodeKey] = pinCode } } override suspend fun deleteEncryptedPinCode() { - context.dataStore.edit { preferences -> + dataStore.edit { preferences -> preferences.remove(pinCodeKey) } } override fun hasPinCode(): Flow { - return context.dataStore.data.map { preferences -> + return dataStore.data.map { preferences -> preferences[pinCodeKey] != null } } override fun isBiometricUnlockAllowed(): Flow { - return context.dataStore.data.map { preferences -> + return dataStore.data.map { preferences -> preferences[biometricUnlockKey] ?: false } } override suspend fun setIsBiometricUnlockAllowed(isAllowed: Boolean) { - context.dataStore.edit { preferences -> + dataStore.edit { preferences -> preferences[biometricUnlockKey] = isAllowed } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockHelper.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockHelper.kt index 0f3da02a7e..59bccff9be 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockHelper.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockHelper.kt @@ -11,13 +11,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState +import dev.zacsweers.metro.Inject import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback import io.element.android.features.lockscreen.impl.pin.PinCodeManager -import javax.inject.Inject -class PinUnlockHelper @Inject constructor( +@Inject +class PinUnlockHelper( private val biometricAuthenticatorManager: BiometricAuthenticatorManager, private val pinCodeManager: PinCodeManager ) { 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 cea53bb844..fba460f6ee 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 @@ -14,13 +14,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class PinUnlockNode @AssistedInject constructor( +@AssistedInject +class PinUnlockNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: PinUnlockPresenter, 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 707ca6b710..fc2e61d404 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 @@ -15,6 +15,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager import io.element.android.features.lockscreen.impl.pin.PinCodeManager @@ -29,9 +30,9 @@ 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 -class PinUnlockPresenter @Inject constructor( +@Inject +class PinUnlockPresenter( private val pinCodeManager: PinCodeManager, private val biometricAuthenticatorManager: BiometricAuthenticatorManager, private val logoutUseCase: LogoutUseCase, @@ -173,7 +174,7 @@ class PinUnlockPresenter @Inject constructor( private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { suspend { - logoutUseCase.logout(ignoreSdkError = true) + logoutUseCase.logoutAll(ignoreSdkError = true) }.runCatchingUpdatingState(signOutAction) } } 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 dd273ebac1..a494bca8c6 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,6 +15,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope +import dev.zacsweers.metro.Inject import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.lockscreen.api.LockScreenLockState import io.element.android.features.lockscreen.api.LockScreenService @@ -26,7 +27,6 @@ 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 -import javax.inject.Inject class PinUnlockActivity : AppCompatActivity() { internal companion object { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt index 9f538dfd3f..8ddb898caa 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt @@ -7,9 +7,9 @@ package io.element.android.features.lockscreen.impl.unlock.di -import com.squareup.anvil.annotations.ContributesTo +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesTo import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity -import io.element.android.libraries.di.AppScope @ContributesTo(AppScope::class) interface PinUnlockBindings { diff --git a/features/lockscreen/impl/src/main/res/values-bg/translations.xml b/features/lockscreen/impl/src/main/res/values-bg/translations.xml index af8e8a9853..7bd2895990 100644 --- a/features/lockscreen/impl/src/main/res/values-bg/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-bg/translations.xml @@ -1,7 +1,12 @@ + "биометрично удостоверяване" + "биометрично отключване" + "Отключване с биометрия" + "Потвърдете биометричните данни" "Забравихте PIN?" "Промяна на PIN кода" + "Разрешаване на биометрично отключване" "Премахване на PIN" "Сигурни ли сте, че искате да премахнете PIN?" "Премахване на PIN?" @@ -9,6 +14,10 @@ "Предпочитам да използвам PIN" "Избор на PIN" "Потвърждаване на PIN" + "Заключете %1$s, за да добавите допълнителна сигурност към вашите чатове. + +Изберете нещо запомнящо се. Ако забравите този PIN, ще бъдете излезли от приложението." + "Не можете да изберете това за ваш PIN код от съображения за сигурност" "Избор на различен PIN" "Моля, въведете един и същ PIN два пъти" "PINs не съвпадат" @@ -20,6 +29,7 @@ "Грешен PIN. Имате още %1$d шанс" "Грешен PIN. Имате още %1$d шанса" + "Използване на биометрия" "Използване на PIN" "Излизане…" 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 f7e06457b4..dd74818610 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" - "Sind Sie sicher, dass Sie die PIN entfernen wollen?" + "Bist du sicher, dass du die PIN entfernen willst?" "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" - "Erhöhen Sie die Sicherheit von %1$s mit einem PIN Code. + "Sperre %1$s um deine Chats zusätzlich abzusichern. -Wählen Sie etwas Einprägsames. Wenn Sie die PIN vergessen, werden Sie aus der App ausgeloggt." +Wähle eine einprägsame PIN. Wenn du sie vergisst, wirst du aus der App abgemeldet." "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, müssen Sie sich erneut anmelden und eine neue PIN erstellen" - "Sie werden abgemeldet" + "Um fortzufahren, musst du dich erneut anmelden und eine neue PIN erstellen" + "Du wirst abgemeldet" - "Sie haben %1$d Entsperrversuch" - "Sie haben %1$d Entsperrversuche" + "Du hast %1$d Versuch, um zu entsperren" + "Du hast %1$d Versuche, um zu entsperren" - "Falsche PIN. Sie haben %1$d weiteren Versuch" - "Falsche PIN. Sie haben %1$d weitere Versuche" + "Falsche PIN. Du hast %1$d weiteren Versuch" + "Falsche PIN. Du hast %1$d weitere Versuche" "Biometrie verwenden" "PIN verwenden" diff --git a/features/lockscreen/impl/src/main/res/values-fi/translations.xml b/features/lockscreen/impl/src/main/res/values-fi/translations.xml index ae2abef6e8..02df7528e5 100644 --- a/features/lockscreen/impl/src/main/res/values-fi/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-fi/translations.xml @@ -9,7 +9,7 @@ "Salli biometrinen tunnistus" "Poista PIN-koodi" "Haluatko varmasti poistaa PIN-koodin?" - "Poista PIN-koodi?" + "Poistetaanko PIN-koodi?" "Salli %1$s" "Käytän mieluummin PIN-koodia" "Säästä aikaa ja ota käyttöön %1$s" diff --git a/features/lockscreen/impl/src/main/res/values-it/translations.xml b/features/lockscreen/impl/src/main/res/values-it/translations.xml index 5f6fa68ff6..514d2461a2 100644 --- a/features/lockscreen/impl/src/main/res/values-it/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-it/translations.xml @@ -15,9 +15,9 @@ "Risparmia un po\' di tempo e usa %1$s per sbloccare l\'app ogni volta" "Scegli il PIN" "Conferma il PIN" - "Blocca %1$s per aggiungere ulteriore sicurezza alle tue conversazioni. + "Blocca %1$s per aggiungere una sicurezza extra alle tue conversazioni. -Scegli qualcosa facile da ricordare. Se dimentichi questo PIN, verrai disconnesso dall\'app." +Scegli un PIN facile da ricordare. Se lo dimentichi, verrai disconnesso dall’app" "Non puoi scegliere questo codice PIN per motivi di sicurezza" "Scegli un PIN diverso" "Inserisci lo stesso PIN due volte" diff --git a/features/lockscreen/impl/src/main/res/values-ko/translations.xml b/features/lockscreen/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..b959841a54 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,36 @@ + + + "생체 인식 인증" + "생체 인식 잠금 해제" + "생체 인증으로 잠금 해제" + "생체 인식 확인" + "PIN을 잊으셨나요?" + "PIN 코드 변경" + "생체 인식 잠금 해제 허용" + "PIN 제거" + "PIN을 제거하시겠습니까?" + "PIN을 제거하시겠습니까?" + "%1$s 허용" + "나는 PIN을 사용하고 싶습니다" + "시간을 절약하려면 %1$s 를 사용하여 앱을 매번 잠금 해제하세요." + "PIN을 선택하세요" + "PIN 확인" + "%1$s 를 잠그면 채팅에 추가 보안이 적용됩니다. + +기억하기 쉬운 것을 선택하세요. 이 PIN을 잊어버리면 앱에서 로그아웃됩니다." + "보안상의 이유로 이 코드를 PIN 코드로 선택할 수 없습니다." + "다른 PIN을 선택하세요" + "PIN을 두 번 입력하세요." + "PIN이 일치하지 않습니다" + "계속하려면 다시 로그인하고 새로운 PIN을 생성해야 합니다" + "로그아웃 중입니다" + + "당신은 %1$d 회 잠금 해제 시도를 가지고 있습니다" + + + "PIN이 잘못되었습니다. %1$d 번 남았습니다" + + "생체 인증 사용" + "PIN 사용" + "로그아웃 중…" + 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 ab3ffc4c06..f2f67dca6d 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 @@ -1,7 +1,7 @@ - "autenticação por biometria" - "desbloqueio por biometria" + "autenticação biométrica" + "desbloqueio biométrico" "Desbloquear com biometria" "Confirmar biometria" "Esqueceu o PIN?" @@ -20,13 +20,13 @@ Escolha algo memorável. Se você esquecer este PIN, você será desconectado do app." "Você não pode escolher este PIN por razões de segurança" "Escolha um PIN diferente" - "Por favor, insira o mesmo PIN duas vezes" + "Por favor, digite o mesmo PIN duas vezes" "Os PINs não correspondem" - "Você terá que fazer login novamente e criar um novo PIN para prosseguir" + "Você terá que entrar novamente e criar um PIN novo para continuar" "Você está sendo desconectado" - "Você tem %1$d tentativa de debloqueio" - "Você tem %1$d tentativas de debloqueio" + "Você tem %1$d tentativa de desbloqueio" + "Você tem %1$d tentativas de desbloqueio" "PIN incorreto. Você tem mais %1$d chance" diff --git a/features/lockscreen/impl/src/main/res/values-ro/translations.xml b/features/lockscreen/impl/src/main/res/values-ro/translations.xml index 69ecc93689..d40bfbaece 100644 --- a/features/lockscreen/impl/src/main/res/values-ro/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-ro/translations.xml @@ -3,12 +3,13 @@ "autentificare biometrică" "deblocare biometrică" "Deblocați cu biometrice" + "Confirmați datele biometrice" "Ați uitat codul PIN?" "Schimbați codul PIN" "Permite deblocarea biometrică" - "Eliminați codul PIN" - "Sunteți sigur că doriți să eliminați codul PIN?" - "Eliminați codul PIN?" + "Ștergeți codul PIN" + "Sunteți sigur că doriți să ștergeți codul PIN?" + "Ștergeți codul PIN?" "Permiteți %1$s" "Prefer să folosesc un cod PIN" "Economisiți timp și utilizați %1$s pentru a debloca aplicația de fiecare dată." diff --git a/features/lockscreen/impl/src/main/res/values-uz/translations.xml b/features/lockscreen/impl/src/main/res/values-uz/translations.xml index e15d51c8bc..9981cf3bce 100644 --- a/features/lockscreen/impl/src/main/res/values-uz/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-uz/translations.xml @@ -2,6 +2,8 @@ "biometrik autentifikatsiya" "biometrik qulf ochish" + "Biometrik bilan qulfni oching" + "PIN kodni unutdingizmi?" "PIN kodni o\'zgartirish" "Biometrik qulfni ochishga ruxsat bering" "PIN-kodni olib tashlang" @@ -29,5 +31,7 @@ Esda qoladigan biror narsani tanlang. Agar ushbu PIN kodni unutib qolsangiz, das "Notoʻgʻri PIN. Sizda yana %1$d ta imkoniyat bor" "Notoʻgʻri PIN. Sizda yana %1$d ta imkoniyat bor" + "Biometrikdan foydalaning" + "PIN koddan foydalaning" "Chiqish…" diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointIntentTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointIntentTest.kt new file mode 100644 index 0000000000..995140b87b --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointIntentTest.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.lockscreen.impl + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DefaultLockScreenEntryPointIntentTest { + @Test + fun `test pin unlock intent`() { + val entryPoint = DefaultLockScreenEntryPoint() + val result = entryPoint.pinUnlockIntent(InstrumentationRegistry.getInstrumentation().context) + assertThat(result.component?.className).isEqualTo(PinUnlockActivity::class.qualifiedName) + } +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointTest.kt new file mode 100644 index 0000000000..822d275063 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointTest.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.features.lockscreen.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.api.LockScreenEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultLockScreenEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder Setup`() { + val entryPoint = DefaultLockScreenEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + LockScreenFlowNode( + buildContext = buildContext, + plugins = plugins, + ) + } + val callback = object : LockScreenEntryPoint.Callback { + override fun onSetupDone() = lambdaError() + } + val navTarget = LockScreenEntryPoint.Target.Setup + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null), navTarget) + .callback(callback) + .build() + assertThat(result).isInstanceOf(LockScreenFlowNode::class.java) + assertThat(result.plugins).contains(LockScreenFlowNode.Inputs(LockScreenFlowNode.NavTarget.Setup)) + assertThat(result.plugins).contains(callback) + } + + @Test + fun `test node builder Settings`() { + val entryPoint = DefaultLockScreenEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + LockScreenFlowNode( + buildContext = buildContext, + plugins = plugins, + ) + } + val callback = object : LockScreenEntryPoint.Callback { + override fun onSetupDone() = lambdaError() + } + val navTarget = LockScreenEntryPoint.Target.Settings + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null), navTarget) + .callback(callback) + .build() + assertThat(result).isInstanceOf(LockScreenFlowNode::class.java) + assertThat(result.plugins).contains(LockScreenFlowNode.Inputs(LockScreenFlowNode.NavTarget.Settings)) + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index ec01f6012a..690fe13578 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -1,5 +1,5 @@ -import extension.ComponentMergingStrategy -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -24,7 +24,7 @@ android { } } -setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP) +setupDependencyInjection() dependencies { implementation(projects.appconfig) @@ -40,6 +40,7 @@ dependencies { implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) implementation(projects.libraries.permissions.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.qrcode) implementation(projects.libraries.oidc.api) implementation(projects.libraries.uiUtils) @@ -49,21 +50,13 @@ dependencies { implementation(libs.serialization.json) api(projects.features.login.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) + testCommonDependencies(libs, true) testImplementation(projects.features.login.test) testImplementation(projects.features.enterprise.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.oidc.test) testImplementation(projects.libraries.permissions.test) + testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.wellknown.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 b6eca77a49..7da2bdf758 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 @@ -10,14 +10,15 @@ package io.element.android.features.login.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.login.api.LoginEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultLoginEntryPoint @Inject constructor() : LoginEntryPoint { +@Inject +class DefaultLoginEntryPoint : LoginEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LoginEntryPoint.NodeBuilder { val plugins = ArrayList() 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 index 4b17980fa6..4851a9ab7c 100644 --- 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 @@ -8,14 +8,15 @@ package io.element.android.features.login.impl import androidx.core.net.toUri -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 { +@Inject +class DefaultLoginIntentResolver : LoginIntentResolver { override fun parse(uriString: String): LoginParams? { val uri = uriString.toUri() if (uri.host != "mobile.element.io") return null diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginUserStory.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginUserStory.kt deleted file mode 100644 index eac64b39e3..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginUserStory.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.features.login.impl - -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.login.api.LoginUserStory -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn -import kotlinx.coroutines.flow.MutableStateFlow -import javax.inject.Inject - -@SingleIn(AppScope::class) -@ContributesBinding(AppScope::class) -class DefaultLoginUserStory @Inject constructor() : LoginUserStory { - // True by default, will be set to false when the login user story is started, and set to true again once it's done. - override val loginFlowIsDone: MutableStateFlow = MutableStateFlow(true) - - fun setLoginFlowIsDone(value: Boolean) { - loginFlowIsDone.value = value - } -} 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 2d791f0f9a..4b83190f5d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -22,9 +22,10 @@ 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 -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource @@ -42,7 +43,6 @@ import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs -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 @@ -51,11 +51,11 @@ import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @ContributesNode(AppScope::class) -class LoginFlowNode @AssistedInject constructor( +@AssistedInject +class LoginFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val accountProviderDataSource: AccountProviderDataSource, - private val defaultLoginUserStory: DefaultLoginUserStory, private val oidcActionFlow: OidcActionFlow, ) : BaseFlowNode( backstack = BackStack( @@ -77,7 +77,6 @@ class LoginFlowNode @AssistedInject constructor( override fun onBuilt() { super.onBuilt() - defaultLoginUserStory.setLoginFlowIsDone(false) lifecycle.subscribe( onResume = { if (externalAppStarted) { @@ -88,7 +87,7 @@ class LoginFlowNode @AssistedInject constructor( // by pressing back or by closing the Custom Chrome Tab. lifecycleScope.launch { delay(5000) - oidcActionFlow.post(OidcAction.GoBack) + oidcActionFlow.post(OidcAction.GoBack(toUnblock = true)) } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControl.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControl.kt index bda1fa1c4b..fb739008a7 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControl.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControl.kt @@ -7,17 +7,18 @@ package io.element.android.features.login.impl.accesscontrol -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl import io.element.android.features.login.impl.changeserver.AccountProviderAccessException import io.element.android.libraries.core.uri.ensureProtocol -import io.element.android.libraries.di.AppScope import io.element.android.libraries.wellknown.api.WellknownRetriever -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultAccountProviderAccessControl @Inject constructor( +@Inject +class DefaultAccountProviderAccessControl( private val enterpriseService: EnterpriseService, private val wellknownRetriever: WellknownRetriever, ) : AccountProviderAccessControl { 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 9ebc246e25..b14dd75b10 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,17 +7,18 @@ package io.element.android.features.login.impl.accountprovider +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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 import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import javax.inject.Inject @SingleIn(AppScope::class) -class AccountProviderDataSource @Inject constructor( +@Inject +class AccountProviderDataSource( enterpriseService: EnterpriseService, ) { private val defaultAccountProvider = 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 3b75ee2578..4df9eb12d5 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 dev.zacsweers.metro.Inject import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource @@ -22,9 +23,9 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import javax.inject.Inject -class ChangeServerPresenter @Inject constructor( +@Inject +class ChangeServerPresenter( private val authenticationService: MatrixAuthenticationService, private val accountProviderDataSource: AccountProviderDataSource, private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt index 13835ea65c..40bb58a96e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt @@ -7,16 +7,16 @@ package io.element.android.features.login.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo import io.element.android.features.login.impl.changeserver.ChangeServerPresenter import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.di.AppScope @ContributesTo(AppScope::class) -@Module +@BindingContainer interface LoginModule { @Binds fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginBindings.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginBindings.kt index cc328d6e86..c3c189f2c8 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginBindings.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginBindings.kt @@ -7,7 +7,7 @@ package io.element.android.features.login.impl.di -import com.squareup.anvil.annotations.ContributesTo +import dev.zacsweers.metro.ContributesTo import io.element.android.features.login.impl.qrcode.QrCodeLoginManager @ContributesTo(QrCodeLoginScope::class) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.kt deleted file mode 100644 index 7f1ffc0285..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.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.login.impl.di - -import com.squareup.anvil.annotations.ContributesTo -import com.squareup.anvil.annotations.MergeSubcomponent -import io.element.android.libraries.architecture.NodeFactoriesBindings -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn - -@SingleIn(QrCodeLoginScope::class) -@MergeSubcomponent(QrCodeLoginScope::class) -interface QrCodeLoginComponent : NodeFactoriesBindings { - @MergeSubcomponent.Builder - interface Builder { - fun build(): QrCodeLoginComponent - } - - @ContributesTo(AppScope::class) - interface ParentBindings { - fun qrCodeLoginComponentBuilder(): Builder - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginGraph.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginGraph.kt new file mode 100644 index 0000000000..12400f97ec --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginGraph.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.login.impl.di + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.GraphExtension +import io.element.android.libraries.architecture.NodeFactoriesBindings + +@GraphExtension(QrCodeLoginScope::class) +interface QrCodeLoginGraph : NodeFactoriesBindings { + @ContributesTo(AppScope::class) + @GraphExtension.Factory + interface Factory { + fun create(): QrCodeLoginGraph + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt index cb61725a4f..82ee87c372 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt @@ -12,7 +12,7 @@ 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 dev.zacsweers.metro.Inject 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 @@ -27,7 +27,6 @@ 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 @@ -35,10 +34,10 @@ import javax.inject.Inject * It's a helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter] * and [ChooseAccountProviderPresenter]. */ -class LoginHelper @Inject constructor( +@Inject +class LoginHelper( private val oidcActionFlow: OidcActionFlow, private val authenticationService: MatrixAuthenticationService, - private val defaultLoginUserStory: DefaultLoginUserStory, private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever, ) { private val loginModeState: MutableState> = mutableStateOf(AsyncData.Uninitialized) @@ -95,9 +94,14 @@ class LoginHelper @Inject constructor( } private suspend fun onOidcAction(oidcAction: OidcAction) { + if (oidcAction is OidcAction.GoBack && oidcAction.toUnblock && loginModeState.value !is AsyncData.Loading) { + // Ignore GoBack action if the current state is not Loading. This GoBack action is coming from LoginFlowNode. + // This can happen if there is an error, for instance attempt to login again on the same account. + return + } loginModeState.value = AsyncData.Loading() when (oidcAction) { - OidcAction.GoBack -> { + is OidcAction.GoBack -> { authenticationService.cancelOidcLogin() .onSuccess { loginModeState.value = AsyncData.Uninitialized @@ -108,9 +112,6 @@ class LoginHelper @Inject constructor( } is OidcAction.Success -> { authenticationService.loginWithOidc(oidcAction.url) - .onSuccess { _ -> - defaultLoginUserStory.setLoginFlowIsDone(true) - } .onFailure { failure -> loginModeState.value = AsyncData.Failure(failure) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt index 73127281bc..c3fe5eac47 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt @@ -14,7 +14,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter 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.error.ChangeServerErrorProvider import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported import io.element.android.libraries.androidutils.system.openGooglePlay import io.element.android.libraries.architecture.AsyncData @@ -23,6 +22,7 @@ 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.LocalBuildMeta +import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.ui.strings.CommonStrings @@ -89,6 +89,12 @@ fun LoginModeView( onSubmit = onClearError, ) } + is AuthenticationException.AccountAlreadyLoggedIn -> { + ErrorDialog( + content = stringResource(CommonStrings.error_account_already_logged_in, error.message.orEmpty()), + onSubmit = onClearError, + ) + } else -> { ErrorDialog( content = stringResource(CommonStrings.error_unknown), @@ -113,7 +119,7 @@ fun LoginModeView( @PreviewsDayNight @Composable -internal fun LoginModeViewPreview(@PreviewParameter(ChangeServerErrorProvider::class) error: ChangeServerError) { +internal fun LoginModeViewPreview(@PreviewParameter(LoginModeViewErrorProvider::class) error: Throwable) { ElementPreview { LoginModeView( loginMode = AsyncData.Failure(error), diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeViewErrorProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeViewErrorProvider.kt new file mode 100644 index 0000000000..dd0a7f353c --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeViewErrorProvider.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.login.impl.login + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.error.ChangeServerErrorProvider +import io.element.android.libraries.matrix.api.auth.AuthenticationException + +class LoginModeViewErrorProvider : PreviewParameterProvider { + override val values: Sequence + get() = ChangeServerErrorProvider().values + + AuthenticationException.AccountAlreadyLoggedIn("@alice:matrix.org") +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManager.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManager.kt index 6628a63254..42bada93a0 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManager.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManager.kt @@ -7,9 +7,10 @@ package io.element.android.features.login.impl.qrcode -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.login.impl.di.QrCodeLoginScope -import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep @@ -17,11 +18,11 @@ import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject @SingleIn(QrCodeLoginScope::class) @ContributesBinding(QrCodeLoginScope::class) -class DefaultQrCodeLoginManager @Inject constructor( +@Inject +class DefaultQrCodeLoginManager( private val authenticationService: MatrixAuthenticationService, ) : QrCodeLoginManager { private val _currentLoginStep = MutableStateFlow(QrCodeLoginStep.Uninitialized) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt index 6956b96464..22dcb9da9c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt @@ -21,12 +21,12 @@ 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 com.bumble.appyx.navmodel.backstack.operation.replace -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.login.impl.DefaultLoginUserStory +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.login.impl.di.QrCodeLoginBindings -import io.element.android.features.login.impl.di.QrCodeLoginComponent +import io.element.android.features.login.impl.di.QrCodeLoginGraph import io.element.android.features.login.impl.screens.qrcode.confirmation.QrCodeConfirmationNode import io.element.android.features.login.impl.screens.qrcode.confirmation.QrCodeConfirmationStep import io.element.android.features.login.impl.screens.qrcode.error.QrCodeErrorNode @@ -38,8 +38,7 @@ import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.createNode import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.DaggerComponentOwner +import io.element.android.libraries.di.DependencyInjectionGraphOwner import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException @@ -50,11 +49,11 @@ import kotlinx.parcelize.Parcelize import timber.log.Timber @ContributesNode(AppScope::class) -class QrCodeLoginFlowNode @AssistedInject constructor( +@AssistedInject +class QrCodeLoginFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - qrCodeLoginComponentBuilder: QrCodeLoginComponent.Builder, - private val defaultLoginUserStory: DefaultLoginUserStory, + qrCodeLoginGraphFactory: QrCodeLoginGraph.Factory, private val coroutineDispatchers: CoroutineDispatchers, ) : BaseFlowNode( backstack = BackStack( @@ -63,10 +62,10 @@ class QrCodeLoginFlowNode @AssistedInject constructor( ), buildContext = buildContext, plugins = plugins, -), DaggerComponentOwner { +), DependencyInjectionGraphOwner { private var authenticationJob: Job? = null - override val daggerComponent = qrCodeLoginComponentBuilder.build() + override val graph = qrCodeLoginGraphFactory.create() private val qrCodeLoginManager by lazy { bindings().qrCodeLoginManager() } sealed interface NavTarget : Parcelable { @@ -198,7 +197,6 @@ class QrCodeLoginFlowNode @AssistedInject constructor( authenticationJob = launch(coroutineDispatchers.main) { qrCodeLoginManager.authenticate(qrCodeLoginData) .onSuccess { - defaultLoginUserStory.setLoginFlowIsDone(true) authenticationJob = null } .onFailure { throwable -> diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt index 56b7391102..5612a56d5e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt @@ -7,6 +7,7 @@ package io.element.android.features.login.impl.resolver +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.parallelMap @@ -21,12 +22,12 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import java.util.Collections -import javax.inject.Inject /** * Resolve homeserver base on search terms. */ -class HomeserverResolver @Inject constructor( +@Inject +class HomeserverResolver( private val dispatchers: CoroutineDispatchers, private val wellknownRetriever: WellknownRetriever, ) { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt index 1ef6508bd2..a0587211c0 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt @@ -14,14 +14,15 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.login.impl.util.openLearnMorePage -import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class ChangeAccountProviderNode @AssistedInject constructor( +@AssistedInject +class ChangeAccountProviderNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: ChangeAccountProviderPresenter, 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 bb3da316b1..3c725106c8 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 @@ -9,6 +9,7 @@ package io.element.android.features.login.impl.screens.changeaccountprovider import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import dev.zacsweers.metro.Inject import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.canConnectToAnyHomeserver @@ -16,9 +17,9 @@ 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( +@Inject +class ChangeAccountProviderPresenter( private val changeServerPresenter: Presenter, private val enterpriseService: EnterpriseService, ) : Presenter { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderNode.kt index 2189252d01..128d235c93 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderNode.kt @@ -14,15 +14,16 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.login.impl.util.openLearnMorePage -import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.OidcDetails @ContributesNode(AppScope::class) -class ChooseAccountProviderNode @AssistedInject constructor( +@AssistedInject +class ChooseAccountProviderNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: ChooseAccountProviderPresenter, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt index 464e30936f..d259454f18 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProvider @@ -20,9 +21,9 @@ 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( +@Inject +class ChooseAccountProviderPresenter( private val enterpriseService: EnterpriseService, private val loginHelper: LoginHelper, ) : Presenter { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt index 975f83375b..0d50d21a18 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt @@ -14,17 +14,18 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.login.impl.util.openLearnMorePage import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.OidcDetails @ContributesNode(AppScope::class) -class ConfirmAccountProviderNode @AssistedInject constructor( +@AssistedInject +class ConfirmAccountProviderNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: ConfirmAccountProviderPresenter.Factory, 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 3bcc81ac83..d485755afb 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 @@ -11,14 +11,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.login.LoginHelper import io.element.android.libraries.architecture.Presenter -class ConfirmAccountProviderPresenter @AssistedInject constructor( +@AssistedInject +class ConfirmAccountProviderPresenter( @Assisted private val params: Params, private val accountProviderDataSource: AccountProviderDataSource, private val loginHelper: LoginHelper, 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 128a4c03e8..44e4bde551 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 @@ -14,17 +14,18 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class CreateAccountNode @AssistedInject constructor( +@AssistedInject +class CreateAccountNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: CreateAccountPresenter.Factory, 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 371c3c3910..d838c971fc 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 @@ -13,10 +13,9 @@ import androidx.compose.runtime.mutableIntStateOf 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.data.tryOrNull @@ -32,11 +31,11 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import kotlin.time.Duration.Companion.seconds -class CreateAccountPresenter @AssistedInject constructor( +@AssistedInject +class CreateAccountPresenter( @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, ) : Presenter { @@ -86,8 +85,6 @@ class CreateAccountPresenter @AssistedInject constructor( 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) }.onFailure { failure -> loggedInState.value = AsyncAction.Failure(failure) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/MessageParser.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/MessageParser.kt index c937cf9d48..8450aef1d3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/MessageParser.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/MessageParser.kt @@ -7,12 +7,12 @@ package io.element.android.features.login.impl.screens.createaccount -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.external.ExternalSession import kotlinx.serialization.json.Json -import javax.inject.Inject interface MessageParser { /** @@ -23,7 +23,8 @@ interface MessageParser { } @ContributesBinding(AppScope::class) -class DefaultMessageParser @Inject constructor( +@Inject +class DefaultMessageParser( private val accountProviderDataSource: AccountProviderDataSource, ) : MessageParser { override fun parse(message: String): ExternalSession { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt index 69b63afce0..ce3d2a84fa 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt @@ -12,13 +12,14 @@ 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.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode @ContributesNode(AppScope::class) -class LoginPasswordNode @AssistedInject constructor( +@AssistedInject +class LoginPasswordNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: LoginPasswordPresenter, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt index 3a12715f6e..80a5711d52 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt @@ -15,7 +15,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable -import io.element.android.features.login.impl.DefaultLoginUserStory +import dev.zacsweers.metro.Inject import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -23,12 +23,11 @@ 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.launch -import javax.inject.Inject -class LoginPasswordPresenter @Inject constructor( +@Inject +class LoginPasswordPresenter( private val authenticationService: MatrixAuthenticationService, private val accountProviderDataSource: AccountProviderDataSource, - private val defaultLoginUserStory: DefaultLoginUserStory, ) : Presenter { @Composable override fun present(): LoginPasswordState { @@ -69,8 +68,6 @@ class LoginPasswordPresenter @Inject constructor( loggedInState.value = AsyncData.Loading() authenticationService.login(formState.login.trim(), formState.password) .onSuccess { sessionId -> - // We will not navigate to the WaitList screen, so the login user story is done - defaultLoginUserStory.setLoginFlowIsDone(true) loggedInState.value = AsyncData.Success(sessionId) } .onFailure { failure -> diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingLogoResIdProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingLogoResIdProvider.kt new file mode 100644 index 0000000000..73ac06bbe2 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingLogoResIdProvider.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.login.impl.screens.onboarding + +import android.annotation.SuppressLint +import android.content.Context +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext + +fun interface OnBoardingLogoResIdProvider { + fun get(): Int? +} + +@ContributesBinding(AppScope::class) +@Inject +class DefaultOnBoardingLogoResIdProvider( + @ApplicationContext private val context: Context, +) : OnBoardingLogoResIdProvider { + @SuppressLint("DiscouragedApi") + override fun get(): Int? { + val resId = context.resources + .getIdentifier("onboarding_logo", "drawable", context.packageName) + .takeIf { it != 0 } + return resId + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt index d9c1615fde..3652a3df8d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt @@ -14,17 +14,18 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.login.impl.util.openLearnMorePage import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.OidcDetails @ContributesNode(AppScope::class) -class OnBoardingNode @AssistedInject constructor( +@AssistedInject +class OnBoardingNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: OnBoardingPresenter.Factory, @@ -96,6 +97,7 @@ class OnBoardingNode @AssistedInject constructor( onNeedLoginPassword = ::onLoginPasswordNeeded, onLearnMoreClick = { openLearnMorePage(context) }, onCreateAccountContinue = ::onCreateAccountContinue, + onBackClick = ::navigateUp, ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt index 0e545f44e6..e7e20aa70d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt @@ -16,9 +16,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.appconfig.OnBoardingConfig import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.canConnectToAnyHomeserver @@ -27,15 +27,19 @@ 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.sessionstorage.api.SessionStore import io.element.android.libraries.ui.utils.MultipleTapToUnlock -class OnBoardingPresenter @AssistedInject constructor( +@AssistedInject +class OnBoardingPresenter( @Assisted private val params: OnBoardingNode.Params, private val buildMeta: BuildMeta, private val enterpriseService: EnterpriseService, private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val loginHelper: LoginHelper, + private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider, + private val sessionStore: SessionStore, ) : Presenter { @AssistedFactory interface Factory { @@ -81,6 +85,13 @@ class OnBoardingPresenter @AssistedInject constructor( } val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false) var showReportBug by rememberSaveable { mutableStateOf(false) } + val onBoardingLogoResId = remember { + onBoardingLogoResIdProvider.get() + } + val isAddingAccount by produceState(initialValue = false) { + // We are adding an account if there is at least one session already stored + value = sessionStore.getAllSessions().isNotEmpty() + } val loginMode by loginHelper.collectLoginMode() @@ -104,6 +115,7 @@ class OnBoardingPresenter @AssistedInject constructor( } return OnBoardingState( + isAddingAccount = isAddingAccount, productionApplicationName = buildMeta.productionApplicationName, defaultAccountProvider = defaultAccountProvider, mustChooseAccountProvider = mustChooseAccountProvider, @@ -112,6 +124,7 @@ class OnBoardingPresenter @AssistedInject constructor( canReportBug = canReportBug && showReportBug, loginMode = loginMode, version = buildMeta.versionName, + onBoardingLogoResId = onBoardingLogoResId, eventSink = ::handleEvent, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt index 1e55c3af2d..ae5bb79eb5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt @@ -7,10 +7,12 @@ package io.element.android.features.login.impl.screens.onboarding +import androidx.annotation.DrawableRes import io.element.android.features.login.impl.login.LoginMode import io.element.android.libraries.architecture.AsyncData data class OnBoardingState( + val isAddingAccount: Boolean, val productionApplicationName: String, val defaultAccountProvider: String?, val mustChooseAccountProvider: Boolean, @@ -18,6 +20,8 @@ data class OnBoardingState( val canCreateAccount: Boolean, val canReportBug: Boolean, val version: String, + @DrawableRes + val onBoardingLogoResId: Int?, val loginMode: AsyncData, val eventSink: (OnBoardingEvents) -> Unit, ) { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt index cdf77da523..2eb9bfb301 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt @@ -7,9 +7,11 @@ package io.element.android.features.login.impl.screens.onboarding +import androidx.annotation.DrawableRes import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.login.LoginMode import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.R open class OnBoardingStateProvider : PreviewParameterProvider { override val values: Sequence @@ -20,10 +22,17 @@ open class OnBoardingStateProvider : PreviewParameterProvider { anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true), anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true, canReportBug = true), anOnBoardingState(defaultAccountProvider = "element.io", canCreateAccount = false, canReportBug = true), + anOnBoardingState(customLogoResId = R.drawable.sample_background), + anOnBoardingState( + isAddingAccount = true, + canLoginWithQrCode = true, + canCreateAccount = true, + ), ) } fun anOnBoardingState( + isAddingAccount: Boolean = false, productionApplicationName: String = "Element", defaultAccountProvider: String? = null, mustChooseAccountProvider: Boolean = false, @@ -31,9 +40,12 @@ fun anOnBoardingState( canCreateAccount: Boolean = false, canReportBug: Boolean = false, version: String = "1.0.0", + @DrawableRes + customLogoResId: Int? = null, loginMode: AsyncData = AsyncData.Uninitialized, eventSink: (OnBoardingEvents) -> Unit = {}, ) = OnBoardingState( + isAddingAccount = isAddingAccount, productionApplicationName = productionApplicationName, defaultAccountProvider = defaultAccountProvider, mustChooseAccountProvider = mustChooseAccountProvider, @@ -42,5 +54,6 @@ fun anOnBoardingState( canReportBug = canReportBug, version = version, loginMode = loginMode, + onBoardingLogoResId = customLogoResId, eventSink = eventSink, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt index 6c67e75cd1..fbc4dc6d09 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt @@ -7,6 +7,7 @@ package io.element.android.features.login.impl.screens.onboarding +import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -19,9 +20,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter @@ -35,7 +38,9 @@ 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.FlowStepPage import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage +import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button @@ -55,6 +60,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun OnBoardingView( state: OnBoardingState, + onBackClick: () -> Unit, onSignInWithQrCode: () -> Unit, onSignIn: (mustChooseAccountProvider: Boolean) -> Unit, onCreateAccount: () -> Unit, @@ -64,34 +70,89 @@ fun OnBoardingView( onCreateAccountContinue: (url: String) -> Unit, onReportProblem: () -> Unit, modifier: Modifier = Modifier, +) { + val loginView = @Composable { + LoginModeView( + loginMode = state.loginMode, + onClearError = { + state.eventSink(OnBoardingEvents.ClearError) + }, + onLearnMoreClick = onLearnMoreClick, + onOidcDetails = onOidcDetails, + onNeedLoginPassword = onNeedLoginPassword, + onCreateAccountContinue = onCreateAccountContinue, + ) + } + val buttons = @Composable { + OnBoardingButtons( + state = state, + onSignInWithQrCode = onSignInWithQrCode, + onSignIn = onSignIn, + onCreateAccount = onCreateAccount, + onReportProblem = onReportProblem, + ) + } + + if (state.isAddingAccount) { + AddOtherAccountScaffold( + modifier = modifier, + loginView = loginView, + buttons = buttons, + onBackClick = onBackClick, + ) + } else { + AddFirstAccountScaffold( + modifier = modifier, + state = state, + loginView = loginView, + buttons = buttons, + ) + } +} + +@Composable +private fun AddFirstAccountScaffold( + state: OnBoardingState, + loginView: @Composable () -> Unit, + buttons: @Composable () -> Unit, + modifier: Modifier = Modifier, ) { OnBoardingPage( modifier = modifier, + renderBackground = state.onBoardingLogoResId == null, content = { - OnBoardingContent(state = state) - LoginModeView( - loginMode = state.loginMode, - onClearError = { - state.eventSink(OnBoardingEvents.ClearError) - }, - onLearnMoreClick = onLearnMoreClick, - onOidcDetails = onOidcDetails, - onNeedLoginPassword = onNeedLoginPassword, - onCreateAccountContinue = onCreateAccountContinue, - ) + if (state.onBoardingLogoResId != null) { + OnBoardingLogo( + onBoardingLogoResId = state.onBoardingLogoResId, + ) + } else { + OnBoardingContent(state = state) + } + loginView() }, footer = { - OnBoardingButtons( - state = state, - onSignInWithQrCode = onSignInWithQrCode, - onSignIn = onSignIn, - onCreateAccount = onCreateAccount, - onReportProblem = onReportProblem, - ) + buttons() } ) } +@Composable +private fun AddOtherAccountScaffold( + loginView: @Composable () -> Unit, + buttons: @Composable () -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + FlowStepPage( + modifier = modifier, + title = stringResource(CommonStrings.common_add_account), + iconStyle = BigIcon.Style.Default(CompoundIcons.HomeSolid()), + buttons = { buttons() }, + content = loginView, + onBackClick = onBackClick, + ) +} + @Composable private fun OnBoardingContent(state: OnBoardingState) { Box( @@ -139,6 +200,24 @@ private fun OnBoardingContent(state: OnBoardingState) { } } +@Composable +private fun OnBoardingLogo( + onBoardingLogoResId: Int, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = onBoardingLogoResId), + contentDescription = null + ) + } +} + @Composable private fun OnBoardingButtons( state: OnBoardingState, @@ -198,27 +277,29 @@ private fun OnBoardingButtons( .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, - ) + if (state.isAddingAccount.not()) { + 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, + ) + } } } } @@ -230,6 +311,7 @@ internal fun OnBoardingViewPreview( ) = ElementPreview { OnBoardingView( state = state, + onBackClick = {}, onSignInWithQrCode = {}, onSignIn = {}, onCreateAccount = {}, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationNode.kt index ce68eec76d..8b8d5b2dd5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationNode.kt @@ -13,14 +13,15 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.login.impl.di.QrCodeLoginScope import io.element.android.libraries.architecture.inputs @ContributesNode(QrCodeLoginScope::class) -class QrCodeConfirmationNode @AssistedInject constructor( +@AssistedInject +class QrCodeConfirmationNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, ) : Node(buildContext = buildContext, plugins = plugins) { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorNode.kt index a1d05e44e6..7b46c2e45c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorNode.kt @@ -13,16 +13,17 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.login.impl.di.QrCodeLoginScope import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType import io.element.android.libraries.architecture.inputs import io.element.android.libraries.core.meta.BuildMeta @ContributesNode(QrCodeLoginScope::class) -class QrCodeErrorNode @AssistedInject constructor( +@AssistedInject +class QrCodeErrorNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val buildMeta: BuildMeta, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroNode.kt index 0c96f614cd..c86dc6096a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroNode.kt @@ -13,13 +13,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.login.impl.di.QrCodeLoginScope @ContributesNode(QrCodeLoginScope::class) -class QrCodeIntroNode @AssistedInject constructor( +@AssistedInject +class QrCodeIntroNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: QrCodeIntroPresenter, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenter.kt index 8a64e6921c..b90e2a6aeb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenter.kt @@ -14,13 +14,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsPresenter -import javax.inject.Inject -class QrCodeIntroPresenter @Inject constructor( +@Inject +class QrCodeIntroPresenter( private val buildMeta: BuildMeta, permissionsPresenterFactory: PermissionsPresenter.Factory, ) : Presenter { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanNode.kt index d5e10d2a82..f6b52522b5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanNode.kt @@ -13,14 +13,15 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.login.impl.di.QrCodeLoginScope import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData @ContributesNode(QrCodeLoginScope::class) -class QrCodeScanNode @AssistedInject constructor( +@AssistedInject +class QrCodeScanNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: QrCodeScanPresenter, 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 9be601f775..ed612f2ac3 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,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.qrcode.QrCodeLoginManager import io.element.android.libraries.architecture.AsyncAction @@ -31,9 +32,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean -import javax.inject.Inject -class QrCodeScanPresenter @Inject constructor( +@Inject +class QrCodeScanPresenter( private val qrCodeLoginDataFactory: MatrixQrCodeLoginDataFactory, private val qrCodeLoginManager: QrCodeLoginManager, private val coroutineDispatchers: CoroutineDispatchers, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt index d6e411393c..dc6084032e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt @@ -14,14 +14,15 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.login.impl.util.openLearnMorePage -import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class SearchAccountProviderNode @AssistedInject constructor( +@AssistedInject +class SearchAccountProviderNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: SearchAccountProviderPresenter, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt index 956d24dc8c..0aa06ca632 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.resolver.HomeserverData import io.element.android.features.login.impl.resolver.HomeserverResolver @@ -23,9 +24,9 @@ import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import javax.inject.Inject -class SearchAccountProviderPresenter @Inject constructor( +@Inject +class SearchAccountProviderPresenter( private val homeserverResolver: HomeserverResolver, private val changeServerPresenter: Presenter, ) : Presenter { 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 8046576347..f72af823f2 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 @@ -8,20 +8,21 @@ package io.element.android.features.login.impl.web import androidx.core.net.toUri -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported -import io.element.android.libraries.di.AppScope import io.element.android.libraries.wellknown.api.WellknownRetriever import timber.log.Timber -import javax.inject.Inject interface WebClientUrlForAuthenticationRetriever { suspend fun retrieve(homeServerUrl: String): String } @ContributesBinding(AppScope::class) -class DefaultWebClientUrlForAuthenticationRetriever @Inject constructor( +@Inject +class DefaultWebClientUrlForAuthenticationRetriever( private val wellknownRetriever: WellknownRetriever, ) : WebClientUrlForAuthenticationRetriever { override suspend fun retrieve(homeServerUrl: String): String { diff --git a/features/login/impl/src/main/res/raw/keep.xml b/features/login/impl/src/main/res/raw/keep.xml new file mode 100644 index 0000000000..478b6d4016 --- /dev/null +++ b/features/login/impl/src/main/res/raw/keep.xml @@ -0,0 +1,12 @@ + + + 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 bd321a9c5a..5d5c23cd49 100644 --- a/features/login/impl/src/main/res/values-bg/translations.xml +++ b/features/login/impl/src/main/res/values-bg/translations.xml @@ -1,6 +1,7 @@ "Промяна на доставчика на акаунт" + "Адрес на сървъра" "Въведете термин за търсене или адрес на домейн." "Потърсете компания, общност или частен сървър." "Намерете доставчик на акаунт" @@ -8,13 +9,19 @@ "На път сте да влезете в %s" "Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли." "На път сте да създадете акаунт в %s" + "Matrix.org е голям, безплатен сървър в публичната мрежа на Matrix за сигурна, децентрализирана комуникация, управляван от фондация Matrix.org." "Друг" "Използвайте друг доставчик на акаунт, като например собствен частен сървър или работен акаунт." "Промяна на доставчика на акаунт" + "Не можахме да достигнем този сървър. Моля, проверете дали сте въвели правилно URL адреса на сървъра. Ако URL адресът е правилен, свържете се с администратора на вашия сървър за допълнителна помощ." + "URL адрес на сървъра" "Какъв е адресът на вашия сървър?" + "Изберете своя сървър" "Създаване на акаунт" "Този акаунт бе деактивиран." "Неправилно потребителско име и/или парола" + "Това не е валиден потребителски идентификатор. Очакван формат: ‘@user:homeserver.org’" + "Избраният сървър не поддържа влизане с парола или OIDC. Моля, свържете се с вашия администратор или изберете друг сървър." "Въведете своите данни" "Matrix е отворена мрежа за сигурна, децентрализирана комуникация." "Добре дошли отново!" @@ -28,6 +35,7 @@ "Повторен опит" "Вашият код за потвърждение" "Промяна на доставчика на акаунт" + "Частен сървър за служителите на Element." "Matrix е отворена мрежа за сигурна, децентрализирана комуникация." "Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли." "На път сте да влезете в %1$s" diff --git a/features/login/impl/src/main/res/values-cs/translations.xml b/features/login/impl/src/main/res/values-cs/translations.xml index b6be1932b8..e5d277c6eb 100644 --- a/features/login/impl/src/main/res/values-cs/translations.xml +++ b/features/login/impl/src/main/res/values-cs/translations.xml @@ -13,6 +13,7 @@ "Jiný" "Použijte jiného poskytovatele účtu, například vlastní soukromý server nebo pracovní účet." "Změnit poskytovatele účtu" + "Google Play" "Na %1$s je vyžadována aplikace Element Pro. Stáhněte si ji prosím z obchodu." "Vyžadován Element Pro" "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." diff --git a/features/login/impl/src/main/res/values-cy/translations.xml b/features/login/impl/src/main/res/values-cy/translations.xml index a9e4b61541..b8988a9889 100644 --- a/features/login/impl/src/main/res/values-cy/translations.xml +++ b/features/login/impl/src/main/res/values-cy/translations.xml @@ -13,6 +13,9 @@ "Arall" "Defnyddiwch ddarparwr cyfrif gwahanol, fel eich gweinydd preifat eich hun neu gyfrif gwaith." "Newid darparwr cyfrif" + "Google Play" + "Mae angen yr ap Element Pro ar %1$s. Llwythwch ef o\'r siop." + "Mae angen Element Pro" "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" 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 6dbfdcf032..da0da5257a 100644 --- a/features/login/impl/src/main/res/values-de/translations.xml +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -6,28 +6,31 @@ "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." - "Sie sind dabei, sich bei %s anzumelden" + "Du bist dabei, dich bei %s anzumelden" "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden." - "Sie sind dabei, ein Konto bei %s zu erstellen." + "Du bist dabei, ein Konto bei %s zu erstellen" "Matrix.org ist ein großer, kostenloser Server im öffentlichen Matrix-Netzwerk für eine sichere, dezentralisierte Kommunikation, der von der Matrix.org Foundation betrieben wird." "Sonstige" "Verwende einen anderen Kontoanbieter, z. B. deinen eigenen privaten Server oder ein Geschäftskonto." "Kontoanbieter wechseln" - "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." + "Google Play" + "Auf %1$s ist die Element Pro App erforderlich. Bitte lade diese aus dem Store." + "Element Pro erforderlich" + "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." "Der Server ist aufgrund eines Problems in der \".well-known\" Datei nicht verfügbar: %1$s" - "Der gewählte Kontoanbieter unterstützt Sliding Sync nicht. Für die Verwendung von %1$s ist ein Upgrade des Servers erforderlich." + "Der gewählte Kontoanbieter unterstützt Sliding-Sync nicht. Für die Verwendung von %1$s ist eine Aktualisierung des Servers erforderlich." "%1$s darf keine Verbindung zu %2$s herstellen." - "Diese App wurde so konfiguriert, dass sie %1$s zulässt." + "Die App wurde so konfiguriert, dass sie %1$s zulässt." "Kontoanbieter %1$s ist nicht zulässig." "Homeserver-URL" - "Geben Sie eine Domainadresse ein." + "Gib eine Domain-Adresse ein." "Wie lautet die Adresse deines Servers?" "Wähle deinen Server aus" "Konto erstellen" "Dieses Konto wurde deaktiviert." - "Falscher Benutzername und/oder Passwort" - "Dies ist keine gültige Benutzerkennung. Erwartetes Format: \'@user:homeserver.org\'" + "Falscher Nutzername und/oder Passwort" + "Dies ist keine gültige Nutzerkennung. Erwartetes Format: \'@nutzer:homeserver.org\'" "Dieser Server ist so konfiguriert, dass er Refresh-Tokens verwendet. Diese werden für die passwortbasierte Anmeldung nicht unterstützt." "Der ausgewählte Homeserver unterstützt weder den Login per Passwort noch per OIDC. Bitte kontaktiere deinen Administrator oder wähle einen anderen Homeserver." "Gib deine Daten ein" @@ -49,7 +52,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" - "Sie werden aufgefordert, die beiden auf diesem Gerät angezeigten Ziffern einzugeben." + "Du wirst aufgefordert, die beiden unten abgebildeten 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" @@ -57,13 +60,13 @@ "Anmeldeanfrage abgebrochen" "Die Anmeldung auf dem anderen Gerät wurde abgelehnt." "Anmelden abgelehnt" - "Die Anmeldung ist abgelaufen. Bitte versuchen Sie es erneut." + "Die Anmeldung ist abgelaufen. Bitte versuche es erneut." "Die Anmeldung wurde nicht rechtzeitig abgeschlossen" "Dein anderes Gerät unterstützt die Anmeldung bei %s mit einem QR-Code nicht. 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." + "Dein Kontoanbieter unterstützt %1$s nicht." "%1$s wird nicht unterstützt" "Bereit zum Scannen" "%1$s auf einem Desktop-Gerät öffnen" @@ -71,25 +74,25 @@ Versuche, dich manuell anzumelden, oder scanne den QR-Code mit einem anderen Ger "Wähle %1$s" "\"Neues Gerät verknüpfen\"" "Scanne den QR-Code mit diesem Gerät" - "Nur verfügbar für den Fall dass Ihr Kontoanbieter dies unterstützt." + "Nur verfügbar falls dein Kontoanbieter dies unterstützt." "Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten" "Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird." "Erneut versuchen" "Falscher QR-Code" "Gehe zu den Kameraeinstellungen" - "Sie müssen %1$s die Erlaubnis erteilen, die Kamera Ihres Geräts zu verwenden um fortzufahren." + "Du musst %1$s die Berechtigung erteilen, die Kamera deines Geräts zu verwenden, um fortzufahren." "Erlaube Zugriff auf die Kamera zum Scannen des QR-Codes" "QR-Code scannen" "Neu beginnen" "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut." "Warten auf dein anderes Gerät" - "Dein Account-Provider kann nach dem folgenden Code fragen, um die Anmeldung zu bestätigen." + "Dein Konto-Provider kann nach dem folgenden Code fragen, um die Anmeldung zu bestätigen." "Dein Verifizierungscode" "Kontoanbieter wechseln" "Ein privater Server für die Mitarbeiter von Element." "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation." "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden." - "Sie sind dabei, sich bei %1$s anzumelden" + "Du bist dabei, dich bei %1$s anzumelden" "Kontoanbieter auswählen" - "Sie sind dabei, ein Konto auf %1$s zu erstellen" + "Du bist dabei, auf %1$s ein Konto zu erstellen" diff --git a/features/login/impl/src/main/res/values-eo/translations.xml b/features/login/impl/src/main/res/values-eo/translations.xml new file mode 100644 index 0000000000..d8ecc9f5d6 --- /dev/null +++ b/features/login/impl/src/main/res/values-eo/translations.xml @@ -0,0 +1,4 @@ + + + "A secure connection could not be made to the new device. Your existing linked devices are still safe and you don\'t need to worry about them." + diff --git a/features/login/impl/src/main/res/values-eu/translations.xml b/features/login/impl/src/main/res/values-eu/translations.xml index 6ab86a1687..ae0079ad39 100644 --- a/features/login/impl/src/main/res/values-eu/translations.xml +++ b/features/login/impl/src/main/res/values-eu/translations.xml @@ -63,6 +63,7 @@ Saiatu saioa eskuz hasten, edo eskaneatu QR kodea beste gailu batean." "Saiatu berriro" "QR kode okerra" "Joan kameraren ezarpenetara" + "Baimendu kameraren sarbidea QR kodea eskaneatzeko" "Eskaneatu QR kodea" "Hasi berriro" "Ustekabeko errore bat gertatu da. Saiatu berriro." 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 b88207f985..310aa22906 100644 --- a/features/login/impl/src/main/res/values-it/translations.xml +++ b/features/login/impl/src/main/res/values-it/translations.xml @@ -13,6 +13,9 @@ "Altro" "Utilizza un provider di account diverso, ad esempio il tuo server privato o un account di lavoro." "Cambia fornitore dell\'account" + "Google Play" + "L\'app Element Pro è necessaria su %1$s. Scaricala dallo store." + "Element Pro è richiesto" "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." "Il server non è disponibile per un problema nel file well-known: %1$s" diff --git a/features/login/impl/src/main/res/values-ko/translations.xml b/features/login/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..2c8e688994 --- /dev/null +++ b/features/login/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,98 @@ + + + "계정 제공자 변경" + "홈서버 주소" + "검색어 또는 도메인 주소를 입력하세요." + "회사, 커뮤니티, 또는 개인 서버를 검색하세요." + "계정 제공자 찾기" + "이곳이 귀하의 대화 공간입니다 — 이메일 제공업체를 사용해 이메일을 관리하는 것처럼 말이죠." + "%s에 로그인합니다" + "이곳이 귀하의 대화 공간입니다 — 이메일 제공업체를 사용해 이메일을 관리하는 것처럼 말이죠." + "%s 에서 계정을 생성하려고 합니다." + "Matrix.org는 Matrix.org 재단이 운영하는, 안전하고 분산된 통신을 위한 공개 Matrix 네트워크의 대규모 무료 서버입니다." + "기타" + "다른 계정 제공업체를 사용하세요. 예를 들어 자체 사설 서버나 업무용 계정 등을 사용할 수 있습니다." + "계정 제공자 변경" + "구글 플레이" + "%1$s 에는 Element Pro 앱이 필요합니다. 스토어에서 다운로드하시기 바랍니다." + "Element Pro가 필요합니다" + "이 홈 서버에 연결할 수 없습니다. 홈 서버 URL을 올바르게 입력했는지 확인하십시오. URL이 올바른 경우 홈 서버 관리자에게 추가 지원을 요청하십시오." + "서버가 .well-known 파일의 문제로 인해 사용할 수 없습니다: +%1$s" + "선택한 계정 제공업체는 sliding sync를 지원하지 않습니다. %1$s를 사용하려면 서버를 업그레이드 해야 합니다." + "%1$s는 %2$s에 연결이 허용되지 않습니다." + "이 앱은 다음을 허용하도록 구성되었습니다: %1$s." + "계정 제공자 %1$s 는 허용되지 않습니다." + "홈서버 URL" + "도메인 주소를 입력하세요." + "서버의 주소는 무엇인가요?" + "서버 선택" + "계정 만들기" + "계정이 비활성화되었습니다." + "잘못된 아이디/비밀번호" + "이 사용자 ID는 유효하지 않습니다. 예상 형식: ‘@user:homeserver.org’" + "이 서버는 새로 고침 토큰을 사용하도록 구성되어 있습니다. 비밀번호 기반 로그인을 사용하는 경우 이 기능은 지원되지 않습니다." + "선택한 홈 서버는 password 또는 OIDC 로그인을 지원하지 않습니다. 관리자에게 문의하거나 다른 홈 서버를 선택하세요." + "귀하의 세부 정보를 입력하십시오" + "Matrix 는 안전하고 분산된 커뮤니케이션을 위한 개방형 네트워크입니다." + "다시 돌아온 걸 환영합니다!" + "%1$s 에 로그인합니다" + "버전 %1$s" + "수동으로 로그인" + "%1$s 에 로그인하세요." + "QR 코드로 로그인" + "계정 만들기" + "%1$s 에 오신 것을 환영합니다. 속도와 단순성을 극대화한 가장 빠른 버전입니다." + "%1$s 에 오신 것을 환영합니다. 속도와 단순성을 위해 최적화된 앱입니다." + "당신의 엘리먼트에 있어" + "안전한 연결 설정" + "새 장치에 안전하게 연결할 수 없습니다. 기존 장치는 여전히 안전하므로 걱정할 필요가 없습니다." + "이제 어떻게 해야 할까?" + "네트워크 문제로 인해 로그인에 실패한 경우 QR 코드로 다시 로그인해 보세요." + "동일한 문제를 겪으신 경우 다른 Wi-Fi 네트워크를 사용해 보거나 Wi-Fi 대신 모바일 데이터를 사용해 보세요." + "만약 작동하지 않는 경우, 수동으로 로그인하세요." + "연결이 안전하지 않습니다" + "이 장치에 표시된 두 자리 숫자를 입력하라는 메시지가 표시됩니다." + "다른 device 에 아래 번호를 입력하세요" + "다른 장치에 로그인한 다음 다시 시도하거나, 이미 로그인되어 있는 다른 장치를 사용하세요." + "로그인하지 않은 다른 장치" + "다른 기기에서 로그인이 취소되었습니다." + "로그인 요청이 취소되었습니다" + "다른 기기에서 로그인이 거부되었습니다." + "로그인 거부됨" + "로그인이 만료되었습니다. 다시 시도해 주세요." + "로그인 시간이 초과되었습니다." + "다른 기기에서는 QR 코드로 %s 에 로그인할 수 없습니다. + +수동으로 로그인하거나 다른 기기로 QR 코드를 스캔해 보세요." + "QR 코드는 지원되지 않습니다" + "귀하의 계정 제공자는 지원하지 않습니다 %1$s ." + "%1$s 지원되지 않습니다" + "스캔 준비 완료" + "데스크톱 장치에서 %1$s 을 엽니다." + "아바타를 클릭하세요" + "선택 %1$s" + "“새로운 기기 연결”" + "이 기기로 QR 코드를 스캔하세요." + "해당 기능은 계정 제공업체가 지원하는 경우에만 사용할 수 있습니다." + "다른 기기에서 %1$s 을 열어 QR 코드를 가져오세요." + "다른 기기에 표시된 QR 코드를 사용하세요." + "다시 시도하기" + "잘못된 QR 코드" + "카메라 설정으로 이동" + "계속하려면 %1$s 가 기기의 카메라를 사용할 수 있도록 권한을 부여해야 합니다." + "카메라 액세스를 허용하여 QR 코드를 스캔하세요" + "QR 코드를 스캔하세요" + "다시 시작하다" + "예기치 않은 오류가 발생했습니다. 다시 시도해 주세요." + "다른 기기를 기다리고 있습니다" + "귀하의 계정 제공자는 로그인을 확인하기 위해 다음 코드를 요청할 수 있습니다." + "귀하의 인증 코드" + "계정 제공자 변경" + "Element 직원을 위한 전용 서버." + "Matrix 는 안전하고 분산된 커뮤니케이션을 위한 개방형 네트워크입니다." + "이곳이 귀하의 대화 공간입니다 — 이메일 제공업체를 사용해 이메일을 관리하는 것처럼 말이죠." + "당신은 %1$s 에 로그인하려 합니다" + "계정 제공자를 선택하세요" + "%1$s 에서 계정을 생성하려고 합니다." + diff --git a/features/login/impl/src/main/res/values-nb/translations.xml b/features/login/impl/src/main/res/values-nb/translations.xml index 003e6966d4..10f554ab91 100644 --- a/features/login/impl/src/main/res/values-nb/translations.xml +++ b/features/login/impl/src/main/res/values-nb/translations.xml @@ -13,6 +13,9 @@ "Annet" "Bruk en annen kontotilbyder, for eksempel din egen private server eller en arbeidskonto." "Bytt kontotilbyder" + "Google Play" + "Element Pro-appen er nødvendig på %1$s. Last den ned fra butikken." + "Element Pro kreves" "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." "Serveren er ikke tilgjengelig på grunn av et problem i den velkjente filen: %1$s" 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 0a6b788bd6..ce67264ae8 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 @@ -1,19 +1,22 @@ "Alterar provedor da conta" - "Endereço do servidor" - "Insira um termo de pesquisa ou um endereço de domínio." + "Endereço do servidor-casa" + "Digite um termo de pesquisa ou o endereço de um domínio." "Procure uma empresa, comunidade ou servidor privado." "Encontre um provedor de contas" - "Aqui é onde suas conversas vão ficar — assim como você usa um provedor de e-mails para manter seus e-mails." + "Aqui é onde suas conversas vão ficar — assim como você usa um provedor de e-mail para guardar seus e-mails." "Você está prestes a entrar em %s" - "Aqui é onde suas conversas vão ficar — assim como você usa um provedor de e-mails para manter seus e-mails." + "Aqui é onde suas conversas vão ficar — assim como você usa um provedor de e-mail para guardar seus e-mails." "Você está prestes a criar uma conta em %s" "O Matrix.org é um grande servidor gratuito na rede pública Matrix para comunicação segura e descentralizada, administrado pela Fundação Matrix.org." "Outro" "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." + "Google Play" + "O app Element Pro é necessário no %1$s. Por favor, baixe-o da loja." + "Element Pro necessário" + "Não conseguimos acessar esse servidor. Verifique se você digitou a URL do servidor corretamente. Se a URL estiver correta, entre em contato com o administrador do seu servidor-casa para obter mais ajuda." "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." @@ -21,75 +24,75 @@ "Este app foi configurado para permitir: %1$s." "O provedor de conta %1$s não é permitido." "URL do servidor" - "Insira um endereço de domínio." + "Digite o endereço de um 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" + "Este servidor está configurado para usar tokens recarregados. Não há suporte a eles ao entrar por uma senha." + "O servidor selecionado não suporta a entrada por senha ou OIDC. Entre em contato com o administrador ou escolha outro servidor." + "Digite seus dados" "A Matrix é uma rede aberta para comunicação segura e descentralizada." - "Bem-vindo de volta!" - "Iniciar sessão em %1$s" + "Boas-vindas novamente!" + "Entrar em %1$s" "Versão %1$s" - "Iniciar sessão manualmente" + "Entrar manualmente" "Entrar em %1$s" - "Iniciar sessão com código QR" + "Entrar com código QR" "Criar conta" - "Bem-vindo ao mais rápido %1$s de todos os tempos. Turbinado para velocidade e simplicidade." + "Boas-vindas ao %1$s mais rápido 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" + "Tente entrar novamente com um código QR caso seja 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" + "Se isso não funcionar, entre manualmente" + "Conexão insegura" "Você será solicitado a inserir os dois dígitos mostrados neste dispositivo." - "Digite o número abaixo em seu outro dispositivo" - "Faça login em seu outro dispositivo e tente novamente, ou use outro dispositivo que já esteja conectado." + "Digite o número abaixo no seu outro dispositivo" + "Entre no 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. + "A entrada foi cancelada no outro dispositivo." + "Solicitação de entrada foi cancelada" + "A entrada foi recusada no outro dispositivo." + "Entrada recusada" + "O processo de entrada expirou. Tente novamente." + "A entrada não foi concluída a tempo" + "Seu outro dispositivo não tem suporte a entrar no %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" +Tente entrar manualmente ou ler o código QR com outro dispositivo." + "Código QR não suportado" + "Seu provedor de conta não tem suporte ao %1$s." + "%1$s não suportado" + "Pronto para ler" + "Abra o %1$s em um computador" "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" + "Disponível somente se o provedor da sua conta ter suporte." + "Abra o %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" + "Você deve permitir que o %1$s use a câmera do seu dispositivo para continuar." + "Permita o acesso à câmera para ler o código QR" "Leia o código QR" - "Comece de novo" + "Começar 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 provedor de conta pode solicitar o seguinte código para verificar a entrada." "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" + "Aqui é onde suas conversas vão ficar — assim como você usa um provedor de e-mail para guardar seus e-mails." + "Você está prestes a entrar 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 c371b79a63..a2a7bdc688 100644 --- a/features/login/impl/src/main/res/values-pt/translations.xml +++ b/features/login/impl/src/main/res/values-pt/translations.xml @@ -24,7 +24,7 @@ "Esta aplicação foi configurada para permitir: %1$s." "Operador de conta %1$s não permitido." "URL do servidor" - "Insere um endereço" + "Introduz um domínio" "Qual é o endereço do teu servidor?" "Seleciona o teu servidor" "Criar conta" @@ -39,7 +39,7 @@ "Iniciar sessão em %1$s" "Versão %1$s" "Iniciar sessão manualmente" - "Iniciar sessão em %1$s" + "Faz login 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." @@ -74,7 +74,7 @@ Tenta iniciar a sessão manualmente ou digitaliza o código QR com outro disposi "Seleciona %1$s" "“Ligar novo dispositivo”" "Lê o código QR com este dispositivo" - "Disponível apenas se o seu fornecedor de conta o suportar." + "Disponível apenas se o teu operador de conta o permitir." "Abre a %1$s noutro dispositivo para obteres o código QR" "Lê o código QR apresentado no outro dispositivo." "Tentar novamente" 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 c1355a9491..6dddc4d0bf 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -13,10 +13,18 @@ "Altul" "Utilizați un alt furnizor de cont, cum ar fi propriul server privat sau un cont de serviciu." "Schimbați furnizorul contului" + "Google Play" + "Aplicația Element Pro este necesară pe %1$s. Descărcați-o din magazin." + "Este necesar Element Pro" "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: + "Serverul nu este disponibil din cauza unei probleme în fișierul well-known: %1$s" + "Furnizorul de cont selectat nu acceptă Sliding sync. Pentru a utiliza funcția „ %1$s ”, este necesară o actualizare a serverului." + "%1$s nu are voie să se conecteze la %2$s." + "Această aplicație a fost configurată pentru a permite: %1$s." + "Furnizorul de cont %1$s nu este permis." "Adresa URL a homeserver-ului" + "Introduceți o adresă de domeniu." "Care este adresa serverului dumneavoastră?" "Selectați serverul dumneavoastra" "Creați un cont" @@ -29,7 +37,9 @@ "Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată." "Bine ați revenit!" "Conectați-vă la %1$s" + "Versiunea %1$s" "Conectați-vă manual" + "Conectați-vă la %1$s" "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." @@ -76,12 +86,13 @@ "Începeți din nou" "A apărut o eroare neașteptată. Vă rugăm să încercați din nou." "În așteptarea celuilalt dispozitiv" - "Furnizorul dumneavoastră de cont poate solicita următorul cod pentru a verifica conectarea." + "Furnizorul dumneavoastră de cont poate cere următorul cod pentru a verifica conectarea." "Codul dumneavoastră de verificare" "Schimbați furnizorul contului" "Un server privat pentru angajații Element." "Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată." "Aici vor trăi conversațiile dvs. - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile." "Sunteți pe cale să vă conectați la %1$s" + "Alegeți furnizorul de cont" "Sunteți pe cale să creați un cont pe %1$s" 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 17aaaa4530..f362537644 100644 --- a/features/login/impl/src/main/res/values-ru/translations.xml +++ b/features/login/impl/src/main/res/values-ru/translations.xml @@ -13,6 +13,8 @@ "Другое" "Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись." "Сменить поставщика учетной записи" + "Требуется приложение Element Pro для %1$s. Пожалуйста, загрузите его из магазина." + "Требуется Element Pro" "Нам не удалось связаться с этим домашним сервером. Убедитесь, что вы правильно ввели URL-адрес домашнего сервера. Если URL-адрес указан правильно, обратитесь к администратору домашнего сервера за дополнительной помощью." "Сервер недоступен из-за проблемы в файле .well-known: %1$s" @@ -34,6 +36,7 @@ "Matrix — это открытая сеть для безопасной децентрализованной связи." "Рады видеть вас снова!" "Войти в %1$s" + "Версия %1$s" "Войти вручную" "Войти в %1$s" "Войти QR-кодом" 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 7d59180093..33fb76b5bd 100644 --- a/features/login/impl/src/main/res/values-sv/translations.xml +++ b/features/login/impl/src/main/res/values-sv/translations.xml @@ -13,6 +13,9 @@ "Annan" "Använd en annan kontoleverantör, till exempel din egen privata server eller ett jobbkonto." "Byt kontoleverantör" + "Google Play" + "Element Pro-appen krävs på %1$s. Ladda ner den från butiken." + "Element Pro krävs" "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: %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 index 1a18fba1bc..cfa7acf206 100644 --- a/features/login/impl/src/main/res/values-tr/translations.xml +++ b/features/login/impl/src/main/res/values-tr/translations.xml @@ -14,7 +14,7 @@ "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: + "Well-known dosyasında bir sorun nedeniyle sunucu kullanılamıyor: %1$s" "Ana sunucu URL\'si" "Sunucunuzun adresi nedir?" 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 dabdf1d3ed..cbf065e925 100644 --- a/features/login/impl/src/main/res/values-uz/translations.xml +++ b/features/login/impl/src/main/res/values-uz/translations.xml @@ -14,6 +14,7 @@ "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." + ".well-known faylidagi muammo tufayli server mavjud emas: %1$s" "Uy serverining URL manzili" "Serveringizning manzili nima?" "Serveringizni tanlang" @@ -21,6 +22,7 @@ "Bu hisob o‘chirilgan." "Notog\'ri foydalanuvchi nomi va/yoki parol" "Bu haqiqiy foydalanuvchi identifikatori emas. Kutilayotgan format: \'@user:homeserver.org\'" + "Ushbu server yangilash tokenlaridan foydalanishga moslashtirilgan. Parolga asoslangan tizimga kirishda bunday tokenlar qoʻllab-quvvatlanmaydi." "Tanlangan uy serveri parol yoki OIDC loginni qo\'lab-quvvatlamaydi. Iltimos, administratoringizga murojaat qiling yoki boshqa uy serverini tanlang." "Tafsilotlaringizni kiriting" "Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir." @@ -32,7 +34,49 @@ "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" + "Xavfsiz aloqa oʻrnatish" + "Yangi qurilmaga xavfsiz ulanish amalga oshirilmadi. Mavjud qurilmalaringiz hali ham xavfsiz va ular haqida qaygʻurishingiz shart emas." + "Endi nima?" + "Agar bu tarmoq muammosi boʻlsa, QR kod bilan qayta kiring" + "Xuddi shu muammoga duch kelsangiz, boshqa wifi tarmogʻini sinang yoki wifi oʻrniga mobil internetdan foydalaning" + "Agar bunisi ishlamasa, oddiy usulda kiring" + "Ulanish xavfsiz emas" + "Sizdan ushbu qurilmada koʻrsatilgan ikkita raqamni kiritish soʻraladi." + "Narigi qurilmada quyidagi raqamni kiriting" + "Boshqa qurilmangizga kiring va qayta urining yoki allaqachon kirilgan boshqa qurilmadan foydalaning." + "Boshqa qurilma tizimga kirmagan" + "Boshqa qurilmadan hisobga kirish bekor qilindi." + "Tizimga kirish soʻrovi bekor qilindi" + "Boshqa qurilmadan hisobga kirish bekor qilindi." + "Tizimga kirish rad etildi" + "Kirish muddati tugagan. Iltimos, qayta urinib koʻring." + "Kirish oʻz vaqtida tugallanmagan" + "Boshqa qurilmangiz %s hisobiga QR kod orqali kirishni qoʻllab-quvvatlamaydi. + +Oddiy usulda kiring yoki boshqa qurilma bilan QR kodni skanerlang." + "QR kod qoʻllab-quvvatlanmaydi" + "Hisob provayderingiz %1$s bilan ishlamaydi." + "%1$s qoʻllab-quvvatlanmaydi" + "Skanerlashga tayyor" + "%1$sʼni kompyuterda oching" + "Avataringizni bosing" + "%1$sʼni tanlang" + "\"Yangi qurilmani bogʻlash\"" + "Bu qurilma bilan QR kodni skanerlang" + "Faqatgina hisob provayderi tomonidan qo‘llab-quvvatlansa mavjud bo‘ladi." + "QR-kodni olish uchun %1$sʼni boshqa qurilmada oching" + "Narigi qurilmada koʻrsatilgan QR koddan foydalaning." "Qayta urinib ko\'ring" + "QR kod notoʻgʻri" + "Kamera sozlamalarini ochish" + "Davom etish uchun %1$s qurilmangiz kamerasidan foydalanishiga ruxsat berishingiz kerak." + "QR kodni skanerlash uchun kameraga ruxsat bering" + "QR kodni skanerlash" + "Qaytadan boshlang" + "Kutilmagan xatolik yuz berdi. Qayta urining." + "Boshqa qurilmangiz kutilmoqda" + "Hisob provayderingiz hisobga kirishni tasdiqlash uchun quyidagi kodni soʻrashi mumkin." + "Tasdiqlash kodingiz" "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/translations.xml b/features/login/impl/src/main/res/values-zh/translations.xml index ec6bc91ff7..de7ea8bc0a 100644 --- a/features/login/impl/src/main/res/values-zh/translations.xml +++ b/features/login/impl/src/main/res/values-zh/translations.xml @@ -13,12 +13,15 @@ "其他" "使用其他账户提供商,例如您自己的私人服务器或工作账户。" "更改账户提供方" + "Google Play" + "%1$s 需要 Element Pro 应用。请从应用商店下载。" "需要 Element Pro 版" "我们无法访问此服务器。请检查您输入的服务器网址是否正确。如果 URL 正确,请联系您的服务器管理员寻求进一步帮助。" "由于 .well-known 文件中存在问题,服务器不可用: %1$s" "所选账户提供商不支持跨屏同步。需要升级服务器才能使用%1$s。" "%1$s不允许连接到%2$s。" + "本应用已配置为允许访问:%1$s 。" "账户提供商%1$s 不被允许。" "服务器网址" "输入域名地址。" diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt new file mode 100644 index 0000000000..c10d22c51a --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.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.features.login.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.test.FakeEnterpriseService +import io.element.android.features.login.api.LoginEntryPoint +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultLoginEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultLoginEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + LoginFlowNode( + buildContext = buildContext, + plugins = plugins, + accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), + oidcActionFlow = FakeOidcActionFlow(), + ) + } + val callback = object : LoginEntryPoint.Callback { + override fun onReportProblem() = lambdaError() + } + val params = LoginEntryPoint.Params( + accountProvider = "ac", + loginHint = "lh", + ) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .callback(callback) + .build() + assertThat(result).isInstanceOf(LoginFlowNode::class.java) + assertThat(result.plugins).contains(LoginFlowNode.Params(params.accountProvider, params.loginHint)) + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeMergedQrCodeLoginComponent.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeMergedQrCodeLoginComponent.kt deleted file mode 100644 index 6cb8ee9ff6..0000000000 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeMergedQrCodeLoginComponent.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.features.login.impl.di - -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.features.login.impl.qrcode.FakeQrCodeLoginManager -import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode -import io.element.android.features.login.impl.qrcode.QrCodeLoginManager -import io.element.android.libraries.architecture.AssistedNodeFactory -import io.element.android.libraries.architecture.createNode - -internal class FakeMergedQrCodeLoginComponent(private val qrCodeLoginManager: QrCodeLoginManager) : - MergedQrCodeLoginComponent { - // Ignore this error, it does override a method once code generation is done - override fun qrCodeLoginManager(): QrCodeLoginManager = qrCodeLoginManager - - class Builder(private val qrCodeLoginManager: QrCodeLoginManager = FakeQrCodeLoginManager()) : - QrCodeLoginComponent.Builder { - override fun build(): QrCodeLoginComponent { - return FakeMergedQrCodeLoginComponent(qrCodeLoginManager) - } - } - - override fun nodeFactories(): Map, AssistedNodeFactory<*>> { - return mapOf( - QrCodeLoginFlowNode::class.java to object : AssistedNodeFactory { - override fun create(buildContext: BuildContext, plugins: List): QrCodeLoginFlowNode { - return createNode(buildContext, plugins) - } - } - ) - } -} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeQrCodeLoginGraph.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeQrCodeLoginGraph.kt new file mode 100644 index 0000000000..5a2ca177e9 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeQrCodeLoginGraph.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.features.login.impl.di + +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.features.login.impl.qrcode.QrCodeLoginFlowNode +import io.element.android.features.login.impl.qrcode.QrCodeLoginManager +import io.element.android.libraries.architecture.AssistedNodeFactory +import kotlin.reflect.KClass + +internal class FakeQrCodeLoginGraph( + private val qrCodeLoginManager: QrCodeLoginManager, +) : QrCodeLoginGraph, QrCodeLoginBindings { + override fun nodeFactories(): Map, AssistedNodeFactory<*>> { + return mapOf( + QrCodeLoginFlowNode::class to object : AssistedNodeFactory { + override fun create(buildContext: BuildContext, plugins: List): QrCodeLoginFlowNode { + error("This factory should not be called in tests") + } + } + ) + } + + override fun qrCodeLoginManager(): QrCodeLoginManager = qrCodeLoginManager + + internal class Builder( + private val qrCodeLoginManager: QrCodeLoginManager, + ) : QrCodeLoginGraph.Factory { + override fun create(): QrCodeLoginGraph { + return FakeQrCodeLoginGraph(qrCodeLoginManager) + } + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt index d01ae8f59e..da2cc8a139 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt @@ -12,8 +12,7 @@ import com.bumble.appyx.core.modality.AncestryInfo import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl import com.google.common.truth.Truth.assertThat -import io.element.android.features.login.impl.DefaultLoginUserStory -import io.element.android.features.login.impl.di.FakeMergedQrCodeLoginComponent +import io.element.android.features.login.impl.di.FakeQrCodeLoginGraph import io.element.android.features.login.impl.screens.qrcode.confirmation.QrCodeConfirmationStep import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep @@ -98,7 +97,7 @@ class QrCodeLoginFlowNodeTest { @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `startAuthentication - success marks the login flow as done`() = runTest { + fun `startAuthentication - success`() = runTest { val fakeAuthenticationService = FakeMatrixAuthenticationService( loginWithQrCodeResult = { _, progress -> progress(QrCodeLoginStep.Finished) @@ -107,12 +106,8 @@ class QrCodeLoginFlowNodeTest { ) // Test with a real manager to ensure the flow is correctly done val qrCodeLoginManager = DefaultQrCodeLoginManager(fakeAuthenticationService) - val defaultLoginUserStory = DefaultLoginUserStory().apply { - loginFlowIsDone.value = false - } val flowNode = createLoginFlowNode( qrCodeLoginManager = qrCodeLoginManager, - defaultLoginUserStory = defaultLoginUserStory, coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) @@ -122,7 +117,6 @@ class QrCodeLoginFlowNodeTest { advanceUntilIdle() assertThat(qrCodeLoginManager.currentLoginStep.value).isEqualTo(QrCodeLoginStep.Finished) - assertThat(defaultLoginUserStory.loginFlowIsDone.value).isTrue() assertThat(flowNode.isLoginInProgress()).isFalse() } @@ -137,12 +131,8 @@ class QrCodeLoginFlowNodeTest { ) // Test with a real manager to ensure the flow is correctly done val qrCodeLoginManager = DefaultQrCodeLoginManager(fakeAuthenticationService) - val defaultLoginUserStory = DefaultLoginUserStory().apply { - loginFlowIsDone.value = false - } val flowNode = createLoginFlowNode( qrCodeLoginManager = qrCodeLoginManager, - defaultLoginUserStory = defaultLoginUserStory, coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) @@ -152,7 +142,6 @@ class QrCodeLoginFlowNodeTest { advanceUntilIdle() assertThat(qrCodeLoginManager.currentLoginStep.value).isEqualTo(QrCodeLoginStep.Failed(QrLoginException.Unknown)) - assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse() assertThat(flowNode.isLoginInProgress()).isFalse() } @@ -167,12 +156,8 @@ class QrCodeLoginFlowNodeTest { ) // Test with a real manager to ensure the flow is correctly done val qrCodeLoginManager = DefaultQrCodeLoginManager(fakeAuthenticationService) - val defaultLoginUserStory = DefaultLoginUserStory().apply { - loginFlowIsDone.value = false - } val flowNode = createLoginFlowNode( qrCodeLoginManager = qrCodeLoginManager, - defaultLoginUserStory = defaultLoginUserStory, coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) @@ -183,13 +168,11 @@ class QrCodeLoginFlowNodeTest { advanceUntilIdle() assertThat(qrCodeLoginManager.currentLoginStep.value).isEqualTo(QrCodeLoginStep.Uninitialized) - assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse() assertThat(flowNode.isLoginInProgress()).isFalse() } private fun TestScope.createLoginFlowNode( qrCodeLoginManager: QrCodeLoginManager = FakeQrCodeLoginManager(), - defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers() ): QrCodeLoginFlowNode { val buildContext = BuildContext( @@ -200,8 +183,7 @@ class QrCodeLoginFlowNodeTest { return QrCodeLoginFlowNode( buildContext = buildContext, plugins = emptyList(), - qrCodeLoginComponentBuilder = FakeMergedQrCodeLoginComponent.Builder(qrCodeLoginManager), - defaultLoginUserStory = defaultLoginUserStory, + qrCodeLoginGraphFactory = FakeQrCodeLoginGraph.Builder(qrCodeLoginManager), coroutineDispatchers = coroutineDispatchers, ) } 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 cbf61f4678..3978d3be6e 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 @@ -13,7 +13,6 @@ 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 @@ -30,7 +29,6 @@ import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oidc.api.OidcActionFlow import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.waitForPredicate import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -119,7 +117,7 @@ class ConfirmAccountProviderPresenterTest { 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) + defaultOidcActionFlow.post(OidcAction.GoBack()) val cancelFailureState = awaitItem() assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } @@ -146,7 +144,30 @@ class ConfirmAccountProviderPresenterTest { assertThat(successState.submitEnabled).isFalse() assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) - defaultOidcActionFlow.post(OidcAction.GoBack) + defaultOidcActionFlow.post(OidcAction.GoBack()) + val cancelFinalState = awaitItem() + assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) + } + } + + @Test + fun `present - oidc - cancel to unblock`() = runTest { + val authenticationService = FakeMatrixAuthenticationService() + val defaultOidcActionFlow = FakeOidcActionFlow() + val presenter = createConfirmAccountProviderPresenter( + matrixAuthenticationService = authenticationService, + defaultOidcActionFlow = defaultOidcActionFlow, + ) + authenticationService.givenHomeserver(A_HOMESERVER_OIDC) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) + val loadingState = awaitItem() + assertThat(loadingState.submitEnabled).isTrue() + assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + defaultOidcActionFlow.post(OidcAction.GoBack(toUnblock = true)) val cancelFinalState = awaitItem() assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) } @@ -186,13 +207,9 @@ class ConfirmAccountProviderPresenterTest { fun `present - oidc - success with success`() = runTest { val authenticationService = FakeMatrixAuthenticationService() val defaultOidcActionFlow = FakeOidcActionFlow() - val defaultLoginUserStory = DefaultLoginUserStory().apply { - setLoginFlowIsDone(false) - } val presenter = createConfirmAccountProviderPresenter( matrixAuthenticationService = authenticationService, defaultOidcActionFlow = defaultOidcActionFlow, - defaultLoginUserStory = defaultLoginUserStory, ) authenticationService.givenHomeserver(A_HOMESERVER_OIDC) moleculeFlow(RecompositionMode.Immediate) { @@ -207,11 +224,9 @@ class ConfirmAccountProviderPresenterTest { assertThat(successState.submitEnabled).isFalse() 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.loginMode).isInstanceOf(AsyncData.Loading::class.java) - waitForPredicate { defaultLoginUserStory.loginFlowIsDone.value } } } @@ -357,7 +372,6 @@ class ConfirmAccountProviderPresenterTest { accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), defaultOidcActionFlow: OidcActionFlow = FakeOidcActionFlow(), - defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), ) = ConfirmAccountProviderPresenter( params = params, @@ -365,7 +379,6 @@ class ConfirmAccountProviderPresenterTest { 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 d97a021fcc..8d70173d11 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 @@ -11,7 +11,6 @@ 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.login.impl.DefaultLoginUserStory import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService @@ -88,9 +87,6 @@ class CreateAccountPresenterTest { @Test fun `present - receiving a message able to be parsed change the state to success`() = runTest { - val defaultLoginUserStory = DefaultLoginUserStory() - defaultLoginUserStory.setLoginFlowIsDone(false) - assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse() val lambda = lambdaRecorder { _ -> anExternalSession() } val sessionVerificationService = FakeSessionVerificationService() val client = FakeMatrixClient(sessionVerificationService = sessionVerificationService) @@ -100,7 +96,6 @@ class CreateAccountPresenterTest { importCreatedSessionLambda = { Result.success(A_SESSION_ID) } ), messageParser = FakeMessageParser(lambda), - defaultLoginUserStory = defaultLoginUserStory, clientProvider = clientProvider, ) moleculeFlow(RecompositionMode.Immediate) { @@ -113,14 +108,10 @@ class CreateAccountPresenterTest { assertThat(awaitItem().createAction.dataOrNull()).isEqualTo(A_SESSION_ID) } lambda.assertions().isCalledOnce().with(value("aMessage")) - assertThat(defaultLoginUserStory.loginFlowIsDone.value).isTrue() } @Test fun `present - receiving a message able to be parsed but error in importing change the state to error`() = runTest { - val defaultLoginUserStory = DefaultLoginUserStory() - defaultLoginUserStory.setLoginFlowIsDone(false) - assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse() val presenter = createPresenter( authenticationService = FakeMatrixAuthenticationService( importCreatedSessionLambda = { Result.failure(AN_EXCEPTION) } @@ -135,20 +126,17 @@ class CreateAccountPresenterTest { assertThat(awaitItem().createAction.isLoading()).isTrue() assertThat(awaitItem().createAction.errorOrNull()).isNotNull() } - assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse() } private fun createPresenter( url: String = "aUrl", authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), - 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/loginpassword/LoginPasswordPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt index 2413e09243..af158ec0da 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 @@ -10,7 +10,6 @@ package io.element.android.features.login.impl.screens.loginpassword 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.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.SessionId @@ -64,12 +63,9 @@ class LoginPasswordPresenterTest { fun `present - submit`() = runTest { val authenticationService = FakeMatrixAuthenticationService() authenticationService.givenHomeserver(A_HOMESERVER) - 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)) initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) @@ -80,7 +76,6 @@ class LoginPasswordPresenterTest { assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java) val loggedInState = awaitItem() assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Success(A_SESSION_ID)) - assertThat(loginUserStory.loginFlowIsDone.value).isTrue() } } @@ -134,10 +129,8 @@ class LoginPasswordPresenterTest { 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/onboarding/DefaultOnBoardingLogoResIdProviderTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/DefaultOnBoardingLogoResIdProviderTest.kt new file mode 100644 index 0000000000..7727583468 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/DefaultOnBoardingLogoResIdProviderTest.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.login.impl.screens.onboarding + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DefaultOnBoardingLogoResIdProviderTest { + @Test + fun `when onboarding_logo resource does not exist, get() returns null`() { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = DefaultOnBoardingLogoResIdProvider(context) + val result = sut.get() + assertThat(result).isNull() + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt index b47adf4cd8..16f6c649fa 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt @@ -11,7 +11,6 @@ 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.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.login.LoginHelper import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever @@ -30,6 +29,9 @@ import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationSer import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.oidc.api.OidcActionFlow import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test @@ -80,10 +82,39 @@ class OnBoardingPresenterTest { assertThat(initialState.productionApplicationName).isEqualTo("B") assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT) assertThat(initialState.canReportBug).isFalse() + assertThat(initialState.isAddingAccount).isFalse() assertThat(awaitItem().canLoginWithQrCode).isTrue() } } + @Test + fun `present - initial state adding account`() = runTest { + val presenter = createPresenter( + sessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData() + ) + ) + ) + presenter.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.isAddingAccount).isTrue() + } + } + + @Test + fun `present - on boarding logo`() = runTest { + val presenter = createPresenter( + onBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { 42 }, + ) + presenter.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.onBoardingLogoResId).isEqualTo(42) + } + } + @Test fun `present - clicking on version 7 times has no effect if rageshake not available`() = runTest { val presenter = createPresenter( @@ -224,6 +255,8 @@ private fun createPresenter( wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(), rageshakeFeatureAvailability: () -> Flow = { flowOf(true) }, loginHelper: LoginHelper = createLoginHelper(), + onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null }, + sessionStore: SessionStore = InMemorySessionStore(), ) = OnBoardingPresenter( params = params, buildMeta = buildMeta, @@ -234,16 +267,16 @@ private fun createPresenter( ), rageshakeFeatureAvailability = rageshakeFeatureAvailability, loginHelper = loginHelper, + onBoardingLogoResIdProvider = onBoardingLogoResIdProvider, + sessionStore = sessionStore, ) 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 index 8ac42b4c93..2f27e2fb2d 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt @@ -25,6 +25,7 @@ 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 @@ -50,6 +51,21 @@ class OnboardingViewTest { } } + @Test + fun `when can go back - clicking on back calls the expected callback`() { + val eventSink = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setOnboardingView( + state = anOnBoardingState( + isAddingAccount = true, + eventSink = eventSink, + ), + onBackClick = callback, + ) + rule.pressBack() + } + } + @Test fun `when can login with QR code - clicking on sign in with QR code calls the expected callback`() { val eventSink = EventsRecorder(expectEvents = false) @@ -235,6 +251,7 @@ class OnboardingViewTest { private fun AndroidComposeTestRule.setOnboardingView( state: OnBoardingState, + onBackClick: () -> Unit = EnsureNeverCalled(), onSignInWithQrCode: () -> Unit = EnsureNeverCalled(), onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(), onCreateAccount: () -> Unit = EnsureNeverCalled(), @@ -247,6 +264,7 @@ class OnboardingViewTest { setContent { OnBoardingView( state = state, + onBackClick = onBackClick, onSignInWithQrCode = onSignInWithQrCode, onSignIn = onSignIn, onCreateAccount = onCreateAccount, 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 6f265ec88c..3d980fe44b 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 @@ -8,16 +8,12 @@ package io.element.android.features.logout.api /** - * Used to trigger a log out of the current user from any part of the app. + * Used to trigger a log out of the current user(s) from any part of the app. */ interface LogoutUseCase { /** - * Log out the current user and then perform any needed cleanup tasks. + * Log out the current user(s) 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. */ - suspend fun logout(ignoreSdkError: Boolean) - - interface Factory { - fun create(sessionId: String): LogoutUseCase - } + suspend fun logoutAll(ignoreSdkError: Boolean) } 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 4d6b6df3d2..08a7047c1c 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 @@ -9,7 +9,7 @@ package io.element.android.features.logout.api.direct import androidx.compose.runtime.Composable -interface DirectLogoutView { +fun interface DirectLogoutView { @Composable fun Render(state: DirectLogoutState) } diff --git a/features/logout/impl/build.gradle.kts b/features/logout/impl/build.gradle.kts index 102b277044..215c273913 100644 --- a/features/logout/impl/build.gradle.kts +++ b/features/logout/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.androidutils) @@ -35,15 +36,8 @@ dependencies { implementation(projects.libraries.dateformatter.api) api(projects.features.logout.api) - 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) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.featureflag.test) - testImplementation(projects.tests.testutils) + testImplementation(projects.libraries.sessionStorage.test) } 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 0c651eb077..47d4771ed9 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 @@ -10,14 +10,15 @@ package io.element.android.features.logout.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.logout.api.LogoutEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultLogoutEntryPoint @Inject constructor() : LogoutEntryPoint { +@Inject +class DefaultLogoutEntryPoint : LogoutEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LogoutEntryPoint.NodeBuilder { val plugins = ArrayList() 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 59906b1d74..52e295ba3e 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 @@ -7,26 +7,36 @@ package io.element.android.features.logout.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.logout.api.LogoutUseCase -import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClientProvider -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import javax.inject.Inject +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.sessionstorage.api.SessionStore +import timber.log.Timber @ContributesBinding(AppScope::class) -class DefaultLogoutUseCase @Inject constructor( - private val authenticationService: MatrixAuthenticationService, +@Inject +class DefaultLogoutUseCase( + private val sessionStore: SessionStore, private val matrixClientProvider: MatrixClientProvider, ) : LogoutUseCase { - override suspend fun logout(ignoreSdkError: Boolean) { - val currentSession = authenticationService.getLatestSessionId() - if (currentSession != null) { - matrixClientProvider.getOrRestore(currentSession) - .getOrThrow() - .logout(userInitiated = true, ignoreSdkError = true) - } else { - error("No session to sign out") - } + override suspend fun logoutAll(ignoreSdkError: Boolean) { + sessionStore.getAllSessions() + .map { sessionData -> + SessionId(sessionData.userId) + } + .forEach { sessionId -> + Timber.d("Logging out sessionId: $sessionId") + matrixClientProvider.getOrRestore(sessionId).fold( + onSuccess = { client -> + client.logout(userInitiated = true, ignoreSdkError = ignoreSdkError) + }, + onFailure = { error -> + Timber.e(error, "Failed to get or restore MatrixClient for sessionId: $sessionId") + } + ) + } } } 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 382ef75b62..d554dbe8f0 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 @@ -13,14 +13,15 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.logout.api.LogoutEntryPoint import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class LogoutNode @AssistedInject constructor( +@AssistedInject +class LogoutNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: LogoutPresenter, 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 3d1cacd89c..a98e300661 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 @@ -16,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -28,9 +29,9 @@ 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 -class LogoutPresenter @Inject constructor( +@Inject +class LogoutPresenter( private val matrixClient: MatrixClient, private val encryptionService: EncryptionService, ) : Presenter { diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/di/LogoutModule.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/di/LogoutModule.kt index 6c5f9a8644..fae2871e4f 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/di/LogoutModule.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/di/LogoutModule.kt @@ -7,16 +7,16 @@ package io.element.android.features.logout.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.impl.direct.DirectLogoutPresenter import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.SessionScope @ContributesTo(SessionScope::class) -@Module +@BindingContainer interface LogoutModule { @Binds fun bindDirectLogoutPresenter(presenter: DirectLogoutPresenter): Presenter 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 e1cf8aa656..2cd7989cf1 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 @@ -9,7 +9,8 @@ package io.element.android.features.logout.impl.direct import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewParameter -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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.DirectLogoutStateProvider @@ -18,10 +19,10 @@ import io.element.android.features.logout.impl.ui.LogoutActionDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.di.SessionScope -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView { +@Inject +class DefaultDirectLogoutView : DirectLogoutView { @Composable override fun Render(state: DirectLogoutState) { val eventSink = state.eventSink 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 4694b6dc2f..1e74aa2bc7 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 @@ -14,6 +14,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import dev.zacsweers.metro.Inject import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.impl.tools.isBackingUp @@ -25,9 +26,9 @@ 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.launch -import javax.inject.Inject -class DirectLogoutPresenter @Inject constructor( +@Inject +class DirectLogoutPresenter( private val matrixClient: MatrixClient, private val encryptionService: EncryptionService, ) : Presenter { 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 a9a74a965d..8ebea45c0e 100644 --- a/features/logout/impl/src/main/res/values-de/translations.xml +++ b/features/logout/impl/src/main/res/values-de/translations.xml @@ -1,18 +1,18 @@ - "Möchten Sie sich wirklich abmelden?" + "Möchtest du dich wirklich abmelden?" "Abmelden" "Abmelden" "Abmelden…" - "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." + "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten." + "Du hast das Backup deaktiviert" + "Das Backup deiner Schlüssel lief noch, als du offline gegangen bist. Verbinde dich erneut, damit deine Schlüssel vor dem Abmelden gesichert werden können." "Deine Schlüssel werden noch gesichert" - "Bitte warten Sie, bis der Vorgang abgeschlossen ist, bevor Sie sich abmelden." + "Bitte warte, bis dieser Vorgang abgeschlossen ist, bevor du dich abmeldest." "Deine Schlüssel werden noch gesichert" "Abmelden" - "Sie sind dabei, sich von Ihrer letzten Sitzung abzumelden. Wenn Sie sich jetzt abmelden, verlieren Sie den Zugriff auf Ihre verschlüsselten Nachrichten." + "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten." "Wiederherstellung nicht eingerichtet" - "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?" + "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du möglicherweise den Zugriff auf deine verschlüsselten Nachrichten." + "Hast du deinen Wiederherstellungsschlüssel gespeichert?" diff --git a/features/logout/impl/src/main/res/values-eo/translations.xml b/features/logout/impl/src/main/res/values-eo/translations.xml new file mode 100644 index 0000000000..5ba5aa6e48 --- /dev/null +++ b/features/logout/impl/src/main/res/values-eo/translations.xml @@ -0,0 +1,8 @@ + + + "Your messages were still being backed up when you went offline. Reconnect so that your messages can be backed up before signing out." + "Your messages are still being backed up" + "Your messages are still being backed up" + "Backup not set up" + "Have you saved your backup password?" + diff --git a/features/logout/impl/src/main/res/values-ko/translations.xml b/features/logout/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..7f6a2f09ee --- /dev/null +++ b/features/logout/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,18 @@ + + + "정말 로그아웃하시겠습니까?" + "로그아웃" + "로그아웃" + "로그아웃 중…" + "마지막 세션에서 로그아웃하려고 합니다. 지금 로그아웃하면 암호화된 메시지에 액세스할 수 없게 됩니다." + "백업이 꺼져 있습니다." + "오프라인으로 전환했을 때 키가 아직 백업 중이었습니다. 로그아웃하기 전에 키를 백업할 수 있도록 다시 연결하세요." + "귀하의 키는 아직 백업 중입니다." + "로그아웃하기 전에 이 과정이 완료될 때까지 기다려 주시기 바랍니다." + "귀하의 키는 아직 백업 중입니다." + "로그아웃" + "마지막 세션에서 로그아웃할 것입니다. 지금 로그아웃하면 암호화된 메시지에 액세스할 수 없게 됩니다." + "복구가 설정되지 않았습니다" + "마지막 세션에서 로그아웃하려고 합니다. 지금 로그아웃하면 암호화된 메시지에 액세스할 수 없게 될 수 있습니다." + "복구 키를 저장하셨습니까?" + diff --git a/features/logout/impl/src/main/res/values-uz/translations.xml b/features/logout/impl/src/main/res/values-uz/translations.xml index f53c9c215b..9cdfc3da2b 100644 --- a/features/logout/impl/src/main/res/values-uz/translations.xml +++ b/features/logout/impl/src/main/res/values-uz/translations.xml @@ -6,6 +6,7 @@ "Chiqish…" "Siz oxirgi sessiyangizdan chiqmoqdasiz. Agar hozir chiqib ketsangiz, shifrlangan xabarlaringizga kira olmaysiz." "Siz zaxira nusxasini oʻchirdingiz" + "Siz oflayn bo‘lganingizda ham kalitlaringiz zaxiralanish jarayonida edi. Tizimdan chiqishdan oldin kalitlaringizning to‘liq zaxiralanishini ta’minlash uchun qayta ulanishingiz zarur." "Kalitlaringiz hamon zaxiralanmoqda" "Tizimdan chiqishdan oldin bu jarayon tugashini kuting." "Kalitlaringiz hamon zaxiralanmoqda" diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPointTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPointTest.kt new file mode 100644 index 0000000000..01d1bfc6ca --- /dev/null +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPointTest.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.logout.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.features.logout.api.LogoutEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultLogoutEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultLogoutEntryPoint() + + val parentNode = TestParentNode.create { buildContext, plugins -> + LogoutNode( + buildContext = buildContext, + plugins = plugins, + presenter = createLogoutPresenter(), + ) + } + val callback = object : LogoutEntryPoint.Callback { + override fun onChangeRecoveryKeyClick() = lambdaError() + } + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .callback(callback) + .build() + assertThat(result).isInstanceOf(LogoutNode::class.java) + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCaseTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCaseTest.kt new file mode 100644 index 0000000000..a17e7285de --- /dev/null +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCaseTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2025 New 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.logout.impl + +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.FakeMatrixClientProvider +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.aSessionData +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultLogoutUseCaseTest { + @Test + fun `test logout from one session`() = runTest { + val logoutLambda1 = lambdaRecorder { _, _ -> } + val client1 = FakeMatrixClient(A_USER_ID).apply { + logoutLambda = logoutLambda1 + } + val sut = DefaultLogoutUseCase( + sessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData(sessionId = A_USER_ID.value), + ) + ), + matrixClientProvider = FakeMatrixClientProvider( + getClient = { sessionId -> + when (sessionId) { + A_USER_ID -> Result.success(client1) + else -> error("Unexpected sessionId") + } + } + ), + ) + sut.logoutAll(ignoreSdkError = true) + logoutLambda1.assertions().isCalledOnce().with(value(true), value(true)) + } + + @Test + fun `test logout from several sessions`() = runTest { + val logoutLambda1 = lambdaRecorder { _, _ -> } + val logoutLambda2 = lambdaRecorder { _, _ -> } + val client1 = FakeMatrixClient(A_USER_ID).apply { + logoutLambda = logoutLambda1 + } + val client2 = FakeMatrixClient(A_USER_ID_2).apply { + logoutLambda = logoutLambda2 + } + val sut = DefaultLogoutUseCase( + sessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData(sessionId = A_USER_ID.value), + aSessionData(sessionId = A_USER_ID_2.value), + ) + ), + matrixClientProvider = FakeMatrixClientProvider( + getClient = { sessionId -> + when (sessionId) { + A_USER_ID -> Result.success(client1) + A_USER_ID_2 -> Result.success(client2) + else -> error("Unexpected sessionId") + } + } + ), + ) + sut.logoutAll(ignoreSdkError = true) + logoutLambda1.assertions().isCalledOnce().with(value(true), value(true)) + logoutLambda2.assertions().isCalledOnce().with(value(true), value(true)) + } + + @Test + fun `test logout session not found is ignored`() = runTest { + val sut = DefaultLogoutUseCase( + sessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData(sessionId = A_USER_ID.value), + ) + ), + matrixClientProvider = FakeMatrixClientProvider( + getClient = { sessionId -> + when (sessionId) { + A_USER_ID -> Result.failure(Exception("Session not found")) + else -> error("Unexpected sessionId") + } + } + ), + ) + sut.logoutAll(ignoreSdkError = true) + // No error + } + + @Test + fun `test logout no sessions`() = runTest { + val sut = DefaultLogoutUseCase( + sessionStore = InMemorySessionStore( + initialList = emptyList() + ), + matrixClientProvider = FakeMatrixClientProvider( + getClient = { sessionId -> + when (sessionId) { + else -> error("Unexpected sessionId") + } + } + ), + ) + sut.logoutAll(ignoreSdkError = true) + // No error + } +} 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 3a309ab7b9..34406aa098 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 @@ -225,12 +225,12 @@ class LogoutPresenterTest { skipItems(2) return awaitItem() } - - private fun createLogoutPresenter( - matrixClient: MatrixClient = FakeMatrixClient(), - encryptionService: EncryptionService = FakeEncryptionService(), - ): LogoutPresenter = LogoutPresenter( - matrixClient = matrixClient, - encryptionService = encryptionService, - ) } + +internal fun createLogoutPresenter( + matrixClient: MatrixClient = FakeMatrixClient(), + encryptionService: EncryptionService = FakeEncryptionService(), +): LogoutPresenter = LogoutPresenter( + matrixClient = matrixClient, + encryptionService = encryptionService, +) 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 e71266b596..dd3ded4ef9 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 @@ -14,7 +14,7 @@ import io.element.android.tests.testutils.simulateLongTask class FakeLogoutUseCase( var logoutLambda: (Boolean) -> Unit = { lambdaError() } ) : LogoutUseCase { - override suspend fun logout(ignoreSdkError: Boolean) = simulateLongTask { + override suspend fun logoutAll(ignoreSdkError: Boolean) = simulateLongTask { logoutLambda(ignoreSdkError) } } diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 1519f204f6..d05ede00ab 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.messages.api) @@ -70,11 +71,7 @@ dependencies { implementation(projects.features.knockrequests.api) implementation(projects.features.roommembermoderation.api) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.libraries.push.test) @@ -83,7 +80,6 @@ dependencies { testImplementation(projects.features.messages.test) testImplementation(projects.services.analytics.test) testImplementation(projects.services.toolbox.test) - testImplementation(projects.tests.testutils) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.mediapickers.test) @@ -93,10 +89,6 @@ dependencies { testImplementation(projects.libraries.mediaplayer.test) testImplementation(projects.libraries.mediaviewer.test) testImplementation(projects.libraries.testtags) - testImplementation(libs.test.mockk) - testImplementation(libs.test.robolectric) testImplementation(projects.features.poll.test) - 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/DefaultMessagesEntryPoint.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt index c167b868e4..b27bcab3b8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt @@ -10,15 +10,18 @@ package io.element.android.features.messages.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 dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.messages.api.MessagesEntryPoint -import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject +import io.element.android.libraries.architecture.NodeFactoriesBindings +import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.di.SessionScope -@ContributesBinding(AppScope::class) -class DefaultMessagesEntryPoint @Inject constructor() : MessagesEntryPoint { +@ContributesBinding(SessionScope::class) +@Inject +class DefaultMessagesEntryPoint : MessagesEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MessagesEntryPoint.NodeBuilder { + val nodeFactories = parentNode.bindings().nodeFactories() val plugins = ArrayList() return object : MessagesEntryPoint.NodeBuilder { @@ -33,7 +36,7 @@ class DefaultMessagesEntryPoint @Inject constructor() : MessagesEntryPoint { } override fun build(): Node { - return parentNode.createNode(buildContext, plugins) + return nodeFactories[MessagesFlowNode::class]!!.create(buildContext, plugins) } } } 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 266d2459f0..af613e2520 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 @@ -15,15 +15,14 @@ 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.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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.Interaction -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode 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 @@ -92,7 +91,8 @@ import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) -class MessagesFlowNode @AssistedInject constructor( +@AssistedInject +class MessagesFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val matrixClient: MatrixClient, @@ -125,9 +125,6 @@ class MessagesFlowNode @AssistedInject constructor( plugins = plugins ) { sealed interface NavTarget : Parcelable { - @Parcelize - data object Empty : NavTarget - @Parcelize data class Messages(val focusedEventId: EventId?) : NavTarget @@ -141,7 +138,7 @@ class MessagesFlowNode @AssistedInject constructor( ) : NavTarget @Parcelize - data class AttachmentPreview(val timelineMode: Timeline.Mode, val attachment: Attachment) : NavTarget + data class AttachmentPreview(val timelineMode: Timeline.Mode, val attachment: Attachment, val inReplyToEventId: EventId?) : NavTarget @Parcelize data class LocationViewer(val location: Location, val description: String?) : NavTarget @@ -223,10 +220,11 @@ class MessagesFlowNode @AssistedInject constructor( ) } - override fun onPreviewAttachments(attachments: ImmutableList) { + override fun onPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) { backstack.push(NavTarget.AttachmentPreview( attachment = attachments.first(), timelineMode = Timeline.Mode.Live, + inReplyToEventId = inReplyToEventId, )) } @@ -313,6 +311,7 @@ class MessagesFlowNode @AssistedInject constructor( val inputs = AttachmentsPreviewNode.Inputs( attachment = navTarget.attachment, timelineMode = navTarget.timelineMode, + inReplyToEventId = navTarget.inReplyToEventId, ) createNode(buildContext, listOf(inputs)) } @@ -396,9 +395,6 @@ class MessagesFlowNode @AssistedInject constructor( } createNode(buildContext, plugins = listOf(callback)) } - NavTarget.Empty -> { - node(buildContext) {} - } NavTarget.KnockRequestsList -> { knockRequestsListEntryPoint.createNode(this, buildContext) } @@ -415,10 +411,11 @@ class MessagesFlowNode @AssistedInject constructor( ) } - override fun onPreviewAttachments(attachments: ImmutableList) { + override fun onPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) { backstack.push(NavTarget.AttachmentPreview( attachment = attachments.first(), - timelineMode = Timeline.Mode.Thread(navTarget.threadRootId) + timelineMode = Timeline.Mode.Thread(navTarget.threadRootId), + inReplyToEventId = inReplyToEventId, )) } @@ -462,6 +459,10 @@ class MessagesFlowNode @AssistedInject constructor( analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) elementCallEntryPoint.startCall(callType) } + + override fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) { + backstack.push(NavTarget.OpenThread(threadRootId, focusedEventId)) + } } createNode(buildContext, listOf(inputs, callback)) } 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 ad8e6c6081..fc417fc029 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 @@ -20,7 +20,7 @@ interface MessagesNavigator { fun onForwardEventClick(eventId: EventId) fun onReportContentClick(eventId: EventId, senderId: UserId) fun onEditPollClick(eventId: EventId) - fun onPreviewAttachment(attachments: ImmutableList) - fun onNavigateToRoom(roomId: RoomId, serverNames: List) + fun onPreviewAttachment(attachments: ImmutableList, inReplyToEventId: EventId?) + fun onNavigateToRoom(roomId: RoomId, eventId: EventId?, serverNames: List) fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) } 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 d286f8a4c7..d4283d19d5 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 @@ -24,9 +24,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.features.knockrequests.api.banner.KnockRequestsBannerRenderer import io.element.android.features.messages.impl.actionlist.ActionListPresenter @@ -50,8 +50,8 @@ 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.ApplicationContext 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 @@ -74,7 +74,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @ContributesNode(RoomScope::class) -class MessagesNode @AssistedInject constructor( +@AssistedInject +class MessagesNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, @ApplicationContext private val context: Context, @@ -91,6 +92,14 @@ class MessagesNode @AssistedInject constructor( private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer, private val roomMemberModerationRenderer: RoomMemberModerationRenderer, ) : Node(buildContext, plugins = plugins), MessagesNavigator { + private val callbacks = plugins() + + data class Inputs( + val focusedEventId: EventId?, + ) : NodeInputs + + private val inputs = inputs() + private val timelineController = TimelineController(room, room.liveTimeline) private val presenter = presenterFactory.create( navigator = this, @@ -98,20 +107,14 @@ class MessagesNode @AssistedInject constructor( timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this), actionListPresenter = actionListPresenterFactory.create( postProcessor = TimelineItemActionPostProcessor.Default, - timelineMode = timelineController.mainTimelineMode() + timelineMode = timelineController.mainTimelineMode(), ), timelineController = timelineController, ) - private val callbacks = plugins() - - data class Inputs(val focusedEventId: EventId?) : NodeInputs - - private val inputs = inputs() interface Callback : Plugin { - fun onRoomDetailsClick() fun onEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean - fun onPreviewAttachments(attachments: ImmutableList) + fun onPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) fun onUserDataClick(userId: UserId) fun onPermalinkClick(data: PermalinkData) fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) @@ -121,9 +124,10 @@ class MessagesNode @AssistedInject constructor( fun onCreatePollClick() fun onEditPollClick(eventId: EventId) fun onJoinCallClick(roomId: RoomId) + fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) + fun onRoomDetailsClick() fun onViewAllPinnedEvents() fun onViewKnockRequests() - fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) } override fun onBuilt() { @@ -142,6 +146,14 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onRoomDetailsClick() } } + private fun onViewAllPinnedMessagesClick() { + callbacks.forEach { it.onViewAllPinnedEvents() } + } + + private fun onViewKnockRequestsClick() { + callbacks.forEach { it.onViewKnockRequests() } + } + private fun onEventClick(timelineMode: Timeline.Mode, 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. @@ -218,15 +230,15 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onEditPollClick(eventId) } } - override fun onPreviewAttachment(attachments: ImmutableList) { - callbacks.forEach { it.onPreviewAttachments(attachments) } + override fun onPreviewAttachment(attachments: ImmutableList, inReplyToEventId: EventId?) { + callbacks.forEach { it.onPreviewAttachments(attachments, inReplyToEventId) } } - override fun onNavigateToRoom(roomId: RoomId, serverNames: List) { + override fun onNavigateToRoom(roomId: RoomId, eventId: EventId?, serverNames: List) { if (roomId == room.roomId) { displaySameRoomToast() } else { - val permalinkData = PermalinkData.RoomLink(roomId.toRoomIdOrAlias(), viaParameters = serverNames.toImmutableList()) + val permalinkData = PermalinkData.RoomLink(roomId.toRoomIdOrAlias(), eventId, viaParameters = serverNames.toImmutableList()) callbacks.forEach { it.onPermalinkClick(permalinkData) } } } @@ -235,10 +247,6 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onOpenThread(threadRootId, focusedEventId) } } - private fun onViewAllPinnedMessagesClick() { - callbacks.forEach { it.onViewAllPinnedEvents() } - } - private fun onSendLocationClick() { callbacks.forEach { it.onSendLocationClick() } } @@ -251,10 +259,6 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onJoinCallClick(room.roomId) } } - private fun onViewKnockRequestsClick() { - callbacks.forEach { it.onViewKnockRequests() } - } - private fun displaySameRoomToast() { context.toast(CommonStrings.screen_room_permalink_same_room_android) } @@ -290,7 +294,15 @@ class MessagesNode @AssistedInject constructor( } }, onUserDataClick = this::onUserDataClick, - onLinkClick = { url, customTab -> onLinkClick(activity, isDark, url, state.timelineState.eventSink, customTab) }, + onLinkClick = { url, customTab -> + onLinkClick( + activity = activity, + darkTheme = isDark, + url = url, + eventSink = state.timelineState.eventSink, + customTab = customTab, + ) + }, onSendLocationClick = this::onSendLocationClick, onCreatePollClick = this::onCreatePollClick, onJoinCallClick = this::onJoinCallClick, 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 380a868aec..46de86cd9e 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 @@ -22,9 +22,9 @@ 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 +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.PinUnpinAction import io.element.android.appconfig.MessageComposerConfig import io.element.android.features.messages.api.timeline.HtmlConverterProvider @@ -43,6 +43,7 @@ import io.element.android.features.messages.impl.timeline.components.customreact import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent @@ -56,6 +57,7 @@ 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.flatMap 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 @@ -63,9 +65,13 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize 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.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.matrix.api.core.toThreadId 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.recentemojis.AddRecentEmoji 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 @@ -89,7 +95,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -class MessagesPresenter @AssistedInject constructor( +@AssistedInject +class MessagesPresenter( @Assisted private val navigator: MessagesNavigator, private val room: JoinedRoom, @Assisted private val composerPresenter: Presenter, @@ -115,6 +122,8 @@ class MessagesPresenter @AssistedInject constructor( private val permalinkParser: PermalinkParser, private val analyticsService: AnalyticsService, private val encryptionService: EncryptionService, + private val featureFlagService: FeatureFlagService, + private val addRecentEmoji: AddRecentEmoji, ) : Presenter { @AssistedFactory interface Factory { @@ -276,8 +285,8 @@ class MessagesPresenter @AssistedInject constructor( } return produceState(UserEventPermissions.DEFAULT, key1 = key) { value = UserEventPermissions( - canSendMessage = room.canSendMessage(type = MessageEventType.ROOM_MESSAGE).getOrElse { true }, - canSendReaction = room.canSendMessage(type = MessageEventType.REACTION).getOrElse { true }, + canSendMessage = room.canSendMessage(type = MessageEventType.RoomMessage).getOrElse { true }, + canSendReaction = room.canSendMessage(type = MessageEventType.Reaction).getOrElse { true }, canRedactOwn = room.canRedactOwn().getOrElse { false }, canRedactOther = room.canRedactOther().getOrElse { false }, canPinUnpin = room.canPinUnpin().getOrElse { false }, @@ -318,8 +327,20 @@ class MessagesPresenter @AssistedInject constructor( TimelineItemAction.AddCaption -> handleActionAddCaption(targetEvent, composerState) TimelineItemAction.EditCaption -> handleActionEditCaption(targetEvent, composerState) TimelineItemAction.RemoveCaption -> handleRemoveCaption(targetEvent) - TimelineItemAction.Reply, - TimelineItemAction.ReplyInThread -> handleActionReply(targetEvent, composerState, timelineProtectionState) + TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState, timelineProtectionState) + TimelineItemAction.ReplyInThread -> { + val displayThreads = featureFlagService.isFeatureEnabled(FeatureFlags.Threads) + if (displayThreads) { + // Get either the thread id this event is in, or the event id if it's not in a thread so we can start one + val threadId = when (targetEvent.threadInfo) { + is TimelineItemThreadInfo.ThreadResponse -> targetEvent.threadInfo.threadRootId + is TimelineItemThreadInfo.ThreadRoot, null -> targetEvent.eventId?.toThreadId() + } ?: return@launch + navigator.onOpenThread(threadId, null) + } else { + handleActionReply(targetEvent, composerState, timelineProtectionState) + } + } TimelineItemAction.ViewSource -> handleShowDebugInfoAction(targetEvent) TimelineItemAction.Forward -> handleForwardAction(targetEvent) TimelineItemAction.ReportContent -> handleReportAction(targetEvent) @@ -380,6 +401,7 @@ class MessagesPresenter @AssistedInject constructor( ) = launch(dispatchers.io) { timelineController.invokeOnCurrentTimeline { toggleReaction(emoji, eventOrTransactionId) + .flatMap { added -> if (added) addRecentEmoji(emoji) else Result.success(Unit) } .onFailure { Timber.e(it) } } } 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 8217cb977c..54b9dc659e 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 @@ -36,7 +36,6 @@ import io.element.android.features.messages.impl.timeline.protection.TimelinePro import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState 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 @@ -49,6 +48,7 @@ import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.aTextEditorStateRich +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentSetOf @@ -60,36 +60,29 @@ open class MessagesStateProvider : PreviewParameterProvider { aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)), aMessagesState(userEventPermissions = aUserEventPermissions(canSendMessage = false)), aMessagesState(showReinvitePrompt = true), - aMessagesState(roomName = null), aMessagesState(composerState = aMessageComposerState(showTextFormatting = true)), aMessagesState( voiceMessageComposerState = aVoiceMessageComposerState(showPermissionRationaleDialog = true), ), - aMessagesState( - roomCallState = anOngoingCallState(), - ), aMessagesState( voiceMessageComposerState = aVoiceMessageComposerState( voiceMessageState = aVoiceMessagePreviewState(), showSendFailureDialog = true ), ), - aMessagesState( - roomCallState = aStandByCallState(canStartCall = false), - ), aMessagesState( pinnedMessagesBannerState = aLoadedPinnedMessagesBannerState( knownPinnedMessagesCount = 4, 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)), - aMessagesState(timelineState = aTimelineState( - timelineMode = Timeline.Mode.Thread(threadRootId = ThreadId("\$a-thread-id")), - timelineItems = aTimelineItemList(aTimelineItemTextContent()), - )), + aMessagesState( + timelineState = aTimelineState( + timelineMode = Timeline.Mode.Thread(threadRootId = ThreadId("\$a-thread-id")), + timelineItems = aTimelineItemList(aTimelineItemTextContent()), + ) + ), ) } @@ -186,9 +179,11 @@ fun aReactionSummaryState( fun aCustomReactionState( target: CustomReactionState.Target = CustomReactionState.Target.None, + recentEmojis: ImmutableList = persistentListOf(), eventSink: (CustomReactionEvents) -> Unit = {}, ) = CustomReactionState( target = target, + recentEmojis = recentEmojis, selectedEmoji = persistentSetOf(), eventSink = eventSink, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index ccbebbbc6d..07fc178721 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 @@ -11,12 +11,10 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -27,27 +25,23 @@ 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.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.role 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.api.timeline.voicemessages.composer.VoiceMessageComposerEvents import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView @@ -66,7 +60,6 @@ import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBan 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.TimelineView -import io.element.android.features.messages.impl.timeline.components.CallMenuItem import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents @@ -74,27 +67,23 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheet 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.topbars.MessagesViewTopBar +import io.element.android.features.messages.impl.topbars.ThreadTopBar import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageSendingFailedDialog import io.element.android.features.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.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.AvatarType -import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayoutState 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 import io.element.android.libraries.designsystem.utils.HideKeyboardWhenDisposed import io.element.android.libraries.designsystem.utils.KeepScreenOn import io.element.android.libraries.designsystem.utils.OnLifecycleEvent @@ -110,7 +99,6 @@ 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.time.Duration.Companion.milliseconds @@ -198,7 +186,13 @@ fun MessagesView( Column { ConnectivityIndicatorView(isOnline = state.hasNetworkConnection) if (state.timelineState.timelineMode is Timeline.Mode.Thread) { - ThreadTopBar(onBackClick = onBackClick) + ThreadTopBar( + roomName = state.roomName, + roomAvatarData = state.roomAvatar, + heroes = state.heroes, + isTombstoned = state.isTombstoned, + onBackClick = onBackClick, + ) } else { MessagesViewTopBar( roomName = state.roomName, @@ -288,7 +282,25 @@ fun MessagesView( ) }, sheetDragHandle = if (state.composerState.showTextFormatting) { - @Composable { BottomSheetDragHandle() } + @Composable { toggleAction -> + val expandA11yLabel = stringResource(CommonStrings.a11y_expand_message_text_field) + val collapseA11yLabel = stringResource(CommonStrings.a11y_collapse_message_text_field) + BottomSheetDragHandle( + modifier = Modifier.semantics { + role = Role.Button + + // Accessibility action to toggle the bottom sheet state + val label = when (expandableState.position) { + ExpandableBottomSheetLayoutState.Position.COLLAPSED, ExpandableBottomSheetLayoutState.Position.DRAGGING -> expandA11yLabel + ExpandableBottomSheetLayoutState.Position.EXPANDED -> collapseA11yLabel + } + onClick(label) { + toggleAction() + true + } + } + ) + } } else { @Composable {} }, @@ -483,120 +495,6 @@ private fun MessagesViewComposerBottomSheetContents( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun MessagesViewTopBar( - roomName: String?, - roomAvatar: AvatarData, - isTombstoned: Boolean, - heroes: ImmutableList, - roomCallState: RoomCallState, - dmUserIdentityState: IdentityState?, - onRoomDetailsClick: () -> Unit, - onJoinCallClick: () -> Unit, - onBackClick: () -> Unit, -) { - TopAppBar( - navigationIcon = { - BackButton(onClick = onBackClick) - }, - title = { - val roundedCornerShape = RoundedCornerShape(8.dp) - Row( - modifier = Modifier - .clip(roundedCornerShape) - .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 - ) - - 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 = { - CallMenuItem( - roomCallState = roomCallState, - onJoinCallClick = onJoinCallClick, - ) - Spacer(Modifier.width(8.dp)) - }, - windowInsets = WindowInsets(0.dp) - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ThreadTopBar( - onBackClick: () -> Unit, -) { - TopAppBar( - navigationIcon = { - BackButton(onClick = onBackClick) - }, - title = { - Text(stringResource(CommonStrings.common_thread)) - } - ) -} - -@Composable -private fun RoomAvatarAndNameRow( - roomName: String?, - roomAvatar: AvatarData, - heroes: ImmutableList, - isTombstoned: Boolean, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - Avatar( - avatarData = roomAvatar, - avatarType = AvatarType.Room( - heroes = heroes, - isTombstoned = isTombstoned, - ), - ) - Text( - 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 - ) - } -} - @Composable private fun CantSendMessageBanner() { Row( 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 fb2cd915bc..25da8c2749 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 @@ -14,10 +14,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import com.squareup.anvil.annotations.ContributesBinding -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesBinding import io.element.android.features.messages.impl.UserEventPermissions import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionComparator @@ -25,12 +25,13 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory 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.TimelineItemThreadInfo import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment 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.TimelineItemRedactedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.canBeCopied import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded @@ -39,7 +40,10 @@ 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.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.recentemojis.GetRecentEmojis import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.preferences.api.store.AppPreferencesStore @@ -59,7 +63,8 @@ interface ActionListPresenter : Presenter { } } -class DefaultActionListPresenter @AssistedInject constructor( +@AssistedInject +class DefaultActionListPresenter( @Assisted private val postProcessor: TimelineItemActionPostProcessor, @Assisted @@ -68,6 +73,8 @@ class DefaultActionListPresenter @AssistedInject constructor( private val room: BaseRoom, private val userSendFailureFactory: VerifiedUserSendFailureFactory, private val dateFormatter: DateFormatter, + private val featureFlagService: FeatureFlagService, + private val getRecentEmojis: GetRecentEmojis, ) : ActionListPresenter { @AssistedFactory @ContributesBinding(RoomScope::class) @@ -95,6 +102,8 @@ class DefaultActionListPresenter @AssistedInject constructor( room.roomInfoFlow.map { it.pinnedEventIds } }.collectAsState(initial = persistentListOf()) + val isThreadsEnabled = featureFlagService.isFeatureEnabledFlow(FeatureFlags.Threads).collectAsState(false) + fun handleEvents(event: ActionListEvents) { when (event) { ActionListEvents.Clear -> target.value = ActionListState.Target.None @@ -104,6 +113,7 @@ class DefaultActionListPresenter @AssistedInject constructor( isDeveloperModeEnabled = isDeveloperModeEnabled, pinnedEventIds = pinnedEventIds, target = target, + isThreadsEnabled = isThreadsEnabled.value, ) } } @@ -119,7 +129,8 @@ class DefaultActionListPresenter @AssistedInject constructor( usersEventPermissions: UserEventPermissions, isDeveloperModeEnabled: Boolean, pinnedEventIds: ImmutableList, - target: MutableState + target: MutableState, + isThreadsEnabled: Boolean, ) = launch { target.value = ActionListState.Target.Loading(timelineItem) @@ -128,6 +139,7 @@ class DefaultActionListPresenter @AssistedInject constructor( usersEventPermissions = usersEventPermissions, isDeveloperModeEnabled = isDeveloperModeEnabled, isEventPinned = pinnedEventIds.contains(timelineItem.eventId), + isThreadsEnabled = isThreadsEnabled, ) val verifiedUserSendFailure = userSendFailureFactory.create(timelineItem.localSendState) @@ -143,26 +155,36 @@ class DefaultActionListPresenter @AssistedInject constructor( ), displayEmojiReactions = displayEmojiReactions, verifiedUserSendFailure = verifiedUserSendFailure, - actions = actions.toImmutableList() + actions = actions.toImmutableList(), + recentEmojis = getRecentEmojis().getOrNull()?.toImmutableList() ?: persistentListOf() ) } else { target.value = ActionListState.Target.None } } - private suspend fun buildActions( + private fun buildActions( timelineItem: TimelineItem.Event, usersEventPermissions: UserEventPermissions, isDeveloperModeEnabled: Boolean, isEventPinned: Boolean, + isThreadsEnabled: Boolean, ): List { val canRedact = timelineItem.isMine && usersEventPermissions.canRedactOwn || !timelineItem.isMine && usersEventPermissions.canRedactOther return buildSet { if (timelineItem.canBeRepliedTo && usersEventPermissions.canSendMessage) { - if (timelineMode !is Timeline.Mode.Thread && timelineItem.threadInfo.threadRootId != null) { + if (isThreadsEnabled && timelineMode !is Timeline.Mode.Thread && timelineItem.isRemote) { + // If threads are enabled, we can reply in thread if the item is remote add(TimelineItemAction.ReplyInThread) - } else { add(TimelineItemAction.Reply) + } else { + if (!isThreadsEnabled && timelineItem.threadInfo is TimelineItemThreadInfo.ThreadResponse) { + // If threads are not enabled, we can reply in a thread if the item is already in the thread + add(TimelineItemAction.ReplyInThread) + } else { + // Otherwise, we can only reply in the room + add(TimelineItemAction.Reply) + } } } if (timelineItem.isRemote && timelineItem.content.canBeForwarded()) { @@ -224,7 +246,7 @@ class DefaultActionListPresenter @AssistedInject constructor( private fun Iterable.postFilter(content: TimelineItemEventContent): Iterable { return filter { action -> when (content) { - is TimelineItemCallNotifyContent, + is TimelineItemRtcNotificationContent, is TimelineItemLegacyCallInviteContent, is TimelineItemStateContent -> action == TimelineItemAction.ViewSource is TimelineItemRedactedContent -> { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt index 8082c3e415..7524a737ff 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt @@ -26,6 +26,7 @@ data class ActionListState( val event: TimelineItem.Event, val sentTimeFull: String, val displayEmojiReactions: Boolean, + val recentEmojis: ImmutableList, val verifiedUserSendFailure: VerifiedUserSendFailure, val actions: ImmutableList, ) : Target diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt index 28e62978de..243df7f055 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt @@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList open class ActionListStateProvider : PreviewParameterProvider { @@ -41,6 +42,7 @@ open class ActionListStateProvider : PreviewParameterProvider { displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -56,6 +58,7 @@ open class ActionListStateProvider : PreviewParameterProvider { actions = aTimelineItemActionList( copyAction = TimelineItemAction.CopyCaption, ), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -70,6 +73,7 @@ open class ActionListStateProvider : PreviewParameterProvider { actions = aTimelineItemActionList( copyAction = TimelineItemAction.CopyCaption, ), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -84,6 +88,7 @@ open class ActionListStateProvider : PreviewParameterProvider { actions = aTimelineItemActionList( copyAction = null, ), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -98,6 +103,7 @@ open class ActionListStateProvider : PreviewParameterProvider { actions = aTimelineItemActionList( copyAction = TimelineItemAction.CopyCaption, ), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -112,6 +118,7 @@ open class ActionListStateProvider : PreviewParameterProvider { actions = aTimelineItemActionList( copyAction = null, ), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -124,6 +131,7 @@ open class ActionListStateProvider : PreviewParameterProvider { displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -136,6 +144,7 @@ open class ActionListStateProvider : PreviewParameterProvider { displayEmojiReactions = false, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), + recentEmojis = persistentListOf(), ), ), anActionListState( @@ -148,6 +157,7 @@ open class ActionListStateProvider : PreviewParameterProvider { displayEmojiReactions = false, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemPollActionList(), + recentEmojis = persistentListOf(), ), ), anActionListState( @@ -160,6 +170,7 @@ open class ActionListStateProvider : PreviewParameterProvider { displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -169,6 +180,7 @@ open class ActionListStateProvider : PreviewParameterProvider { displayEmojiReactions = true, verifiedUserSendFailure = anUnsignedDeviceSendFailure(), actions = aTimelineItemActionList(), + recentEmojis = persistentListOf(), ) ), ) 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 78b4b43112..a891e9d587 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 @@ -13,6 +13,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -20,9 +21,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api @@ -35,6 +38,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -56,7 +63,6 @@ import io.element.android.features.messages.impl.timeline.a11y.a11yReactionActio 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 -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent @@ -64,6 +70,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent @@ -90,6 +97,8 @@ import io.element.android.libraries.matrix.ui.messages.sender.SenderName import io.element.android.libraries.matrix.ui.messages.sender.SenderNameMode import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -218,6 +227,7 @@ private fun ActionListViewContent( if (target.displayEmojiReactions) { item { EmojiReactionsRow( + recentEmojis = target.recentEmojis, highlightedEmojis = target.event.reactionsState.highlightedKeys, onEmojiReactionClick = onEmojiReactionClick, onCustomReactionClick = onCustomReactionClick, @@ -306,7 +316,7 @@ private fun MessageSummary( is TimelineItemLegacyCallInviteContent -> { content = { ContentForBody(textContent) } } - is TimelineItemCallNotifyContent -> { + is TimelineItemRtcNotificationContent -> { content = { ContentForBody(stringResource(CommonStrings.common_call_started)) } } } @@ -335,43 +345,67 @@ private fun MessageSummary( } private val emojiRippleRadius = 24.dp +private val suggestedEmojis = persistentListOf("👍️", "👎️", "🔥", "❤️", "👏") @Composable private fun EmojiReactionsRow( + recentEmojis: ImmutableList, highlightedEmojis: ImmutableList, onEmojiReactionClick: (String) -> Unit, onCustomReactionClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = modifier.padding(horizontal = 24.dp, vertical = 16.dp) + modifier = modifier.padding(end = 16.dp, top = 16.dp, bottom = 16.dp), ) { - // TODO use most recently used emojis here when available from the Rust SDK - val defaultEmojis = sequenceOf( - "👍️", - "👎️", - "🔥", - "❤️", - "👏" - ) - for (emoji in defaultEmojis) { - val isHighlighted = highlightedEmojis.contains(emoji) - EmojiButton( - modifier = Modifier - // Make it appear after the more useful actions for the accessibility service - .semantics { - traversalIndex = 1f - }, - emoji = emoji, - isHighlighted = isHighlighted, - onClick = onEmojiReactionClick - ) + val backgroundColor = ElementTheme.colors.bgCanvasDefault + + val emojis = remember(recentEmojis) { + (suggestedEmojis + recentEmojis.filter { it !in suggestedEmojis }) + .take(100) + .toImmutableList() } - Box( + + LazyRow( modifier = Modifier - .size(48.dp), - contentAlignment = Alignment.Center, + .weight(1f, fill = true) + .drawWithContent { + val gradientWidth = 24.dp.toPx() + val width = size.width + drawContent() + + drawRect( + brush = Brush.horizontalGradient( + 0.0f to Color.Transparent, + 1.0f to backgroundColor, + startX = width - gradientWidth, + endX = width, + ), + topLeft = Offset(width - gradientWidth, 0f), + size = Size(gradientWidth, size.height) + ) + }, + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(emojis) { emoji -> + val isHighlighted = highlightedEmojis.contains(emoji) + 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.padding(end = 10.dp).requiredSize(48.dp), + contentAlignment = Alignment.CenterEnd, ) { Icon( imageVector = CompoundIcons.ReactionAdd(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt index 735d5548e8..1df9969f72 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt @@ -12,19 +12,21 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ForcedDarkElementTheme import io.element.android.features.messages.impl.attachments.Attachment 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.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer @ContributesNode(RoomScope::class) -class AttachmentsPreviewNode @AssistedInject constructor( +@AssistedInject +class AttachmentsPreviewNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: AttachmentsPreviewPresenter.Factory, @@ -33,6 +35,7 @@ class AttachmentsPreviewNode @AssistedInject constructor( data class Inputs( val attachment: Attachment, val timelineMode: Timeline.Mode, + val inReplyToEventId: EventId?, ) : NodeInputs private val inputs: Inputs = inputs() @@ -45,6 +48,7 @@ class AttachmentsPreviewNode @AssistedInject constructor( attachment = inputs.attachment, timelineMode = inputs.timelineMode, onDoneListener = onDoneListener, + inReplyToEventId = inputs.inReplyToEventId, ) @Composable 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 10f99b7641..34a4f12fe5 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 @@ -17,13 +17,14 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.video.MediaOptimizationSelectorPresenter import io.element.android.libraries.androidutils.file.TemporaryUriDeleter import io.element.android.libraries.androidutils.file.safeDelete +import io.element.android.libraries.androidutils.hash.hash import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.firstInstanceOf @@ -48,10 +49,12 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import timber.log.Timber -class AttachmentsPreviewPresenter @AssistedInject constructor( +@AssistedInject +class AttachmentsPreviewPresenter( @Assisted private val attachment: Attachment, @Assisted private val onDoneListener: OnDoneListener, @Assisted private val timelineMode: Timeline.Mode, + @Assisted private val inReplyToEventId: EventId?, mediaSenderFactory: MediaSender.Factory, private val permalinkBuilder: PermalinkBuilder, private val temporaryUriDeleter: TemporaryUriDeleter, @@ -65,6 +68,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( attachment: Attachment, timelineMode: Timeline.Mode, onDoneListener: OnDoneListener, + inReplyToEventId: EventId?, ): AttachmentsPreviewPresenter } @@ -180,7 +184,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( caption = caption, sendActionState = sendActionState, dismissAfterSend = false, - inReplyToEventId = null, + inReplyToEventId = inReplyToEventId, ) // Clean up the pre-processed media after it's been sent @@ -261,6 +265,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( mediaOptimizationConfig = mediaOptimizationConfig, ).fold( onSuccess = { mediaUploadInfo -> + Timber.d("Media ${mediaUploadInfo.file.path.orEmpty().hash()} finished processing, it's now ready to upload") sendActionState.value = SendActionState.Sending.ReadyToUpload(mediaUploadInfo) }, onFailure = { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt index ea9c3dcc0c..49d965030f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt @@ -14,10 +14,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import com.squareup.anvil.annotations.ContributesBinding -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo import io.element.android.libraries.di.SessionScope @@ -34,7 +34,8 @@ import kotlinx.coroutines.flow.first import timber.log.Timber import kotlin.math.roundToLong -class DefaultMediaOptimizationSelectorPresenter @AssistedInject constructor( +@AssistedInject +class DefaultMediaOptimizationSelectorPresenter( @Assisted private val localMedia: LocalMedia, private val maxUploadSizeProvider: MaxUploadSizeProvider, private val sessionPreferencesStore: SessionPreferencesStore, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/VideoMetadataExtractor.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/VideoMetadataExtractor.kt index 0567bfeffc..a63668acaa 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/VideoMetadataExtractor.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/VideoMetadataExtractor.kt @@ -11,13 +11,13 @@ import android.content.Context import android.media.MediaMetadataRetriever import android.net.Uri import android.util.Size -import com.squareup.anvil.annotations.ContributesBinding -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesBinding 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.annotations.ApplicationContext import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -30,7 +30,8 @@ interface VideoMetadataExtractor : AutoCloseable { } @ContributesBinding(AppScope::class) -class DefaultVideoMetadataExtractor @AssistedInject constructor( +@AssistedInject +class DefaultVideoMetadataExtractor( @ApplicationContext private val context: Context, @Assisted private val uri: Uri, ) : VideoMetadataExtractor { 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 1d3b9778c3..85675d784e 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 @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.rememberCoroutineScope +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.EncryptionService @@ -20,9 +21,9 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject -class IdentityChangeStatePresenter @Inject constructor( +@Inject +class IdentityChangeStatePresenter( private val room: JoinedRoom, private val encryptionService: EncryptionService, ) : Presenter { 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 1052332403..09f85805c8 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,11 +7,12 @@ package io.element.android.features.messages.impl.crypto.sendfailure +import dev.zacsweers.metro.Inject 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( +@Inject +class VerifiedUserSendFailureFactory( private val room: BaseRoom, ) { suspend fun create( 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 0373002f7e..54aa26e6ab 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 @@ -14,6 +14,7 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory import io.element.android.libraries.architecture.AsyncAction @@ -22,9 +23,9 @@ import io.element.android.libraries.architecture.runUpdatingState 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( +@Inject +class ResolveVerifiedUserSendFailurePresenter( private val room: JoinedRoom, private val verifiedUserSendFailureFactory: VerifiedUserSendFailureFactory, ) : Presenter { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesBindsModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesBindsModule.kt index 5148ea0216..50f9606273 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesBindsModule.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesBindsModule.kt @@ -7,9 +7,9 @@ package io.element.android.features.messages.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStatePresenter import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailurePresenter @@ -32,7 +32,7 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope @ContributesTo(RoomScope::class) -@Module +@BindingContainer interface MessagesBindsModule { @Binds fun bindPinnedMessagesBannerPresenter(presenter: PinnedMessagesBannerPresenter): Presenter diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesProvidesModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesProvidesModule.kt index 970aa63b75..d353ae2c09 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesProvidesModule.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesProvidesModule.kt @@ -7,16 +7,16 @@ package io.element.android.features.messages.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides import io.element.android.features.messages.impl.timeline.di.LiveTimeline import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.Timeline @ContributesTo(RoomScope::class) -@Module +@BindingContainer object MessagesProvidesModule { @Provides @LiveTimeline 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 57909c5a8a..a0cb3877cb 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 @@ -7,15 +7,16 @@ package io.element.android.features.messages.impl.draft -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 @ContributesBinding(RoomScope::class) -class DefaultComposerDraftService @Inject constructor( +@Inject +class DefaultComposerDraftService( private val volatileComposerDraftStore: VolatileComposerDraftStore, private val matrixComposerDraftStore: MatrixComposerDraftStore, ) : ComposerDraftService { 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 88000546dd..6ce82b6522 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 @@ -7,18 +7,19 @@ package io.element.android.features.messages.impl.draft +import dev.zacsweers.metro.Inject 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 /** * A draft store that persists drafts in the room state. * It can be used to store drafts that should be persisted across app restarts. */ -class MatrixComposerDraftStore @Inject constructor( +@Inject +class MatrixComposerDraftStore( private val client: MatrixClient, ) : ComposerDraftStore { override suspend fun loadDraft(roomId: RoomId, threadRoot: ThreadId?): ComposerDraft? { 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 b7b714f5c9..138763bd13 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 @@ -7,17 +7,18 @@ package io.element.android.features.messages.impl.draft +import dev.zacsweers.metro.Inject 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 /** * A volatile draft store that keeps drafts in memory only. * It can be used to store drafts that should not be persisted across app restarts. * Currently it's used to store draft message when moving to edit mode. */ -class VolatileComposerDraftStore @Inject constructor() : ComposerDraftStore { +@Inject +class VolatileComposerDraftStore : ComposerDraftStore { private val drafts: MutableMap = mutableMapOf() override suspend fun loadDraft(roomId: RoomId, threadRoot: ThreadId?): ComposerDraft? { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt index 3f6e6c3efb..2cf0c6e1e7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt @@ -17,9 +17,9 @@ import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.core.plugin.Plugin -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope @@ -31,7 +31,8 @@ import io.element.android.libraries.roomselect.api.RoomSelectMode import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) -class ForwardMessagesNode @AssistedInject constructor( +@AssistedInject +class ForwardMessagesNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: ForwardMessagesPresenter.Factory, 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 013003e4b7..b785f83c66 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 @@ -10,9 +10,9 @@ package io.element.android.features.messages.impl.forward import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState @@ -26,7 +26,8 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class ForwardMessagesPresenter @AssistedInject constructor( +@AssistedInject +class ForwardMessagesPresenter( @Assisted eventId: String, @Assisted private val timelineProvider: TimelineProvider, @SessionCoroutineScope diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkChecker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkChecker.kt index 6bf24642bc..eec953f4cd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkChecker.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkChecker.kt @@ -7,20 +7,21 @@ package io.element.android.features.messages.impl.link -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.extensions.containsRtLOverride -import io.element.android.libraries.di.AppScope import io.element.android.wysiwyg.link.Link import java.net.URI -import javax.inject.Inject interface LinkChecker { fun isSafe(link: Link): Boolean } @ContributesBinding(AppScope::class) -class DefaultLinkChecker @Inject constructor() : LinkChecker { +@Inject +class DefaultLinkChecker : LinkChecker { override fun isSafe(link: Link): Boolean { return if (link.url.containsRtLOverride()) { false diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkPresenter.kt index 3259bdf8f8..9c4694bafa 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkPresenter.kt @@ -11,12 +11,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.wysiwyg.link.Link -import javax.inject.Inject -class LinkPresenter @Inject constructor( +@Inject +class LinkPresenter( private val linkChecker: LinkChecker, ) : Presenter { @Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/DefaultMessageComposerContext.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/DefaultMessageComposerContext.kt index 758f7013a2..b895572c43 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/DefaultMessageComposerContext.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/DefaultMessageComposerContext.kt @@ -10,16 +10,17 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.messages.api.MessageComposerContext import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.di.SingleIn import io.element.android.libraries.textcomposer.model.MessageComposerMode -import javax.inject.Inject @SingleIn(RoomScope::class) @ContributesBinding(RoomScope::class) -class DefaultMessageComposerContext @Inject constructor() : MessageComposerContext { +@Inject +class DefaultMessageComposerContext : MessageComposerContext { override var composerMode: MessageComposerMode by mutableStateOf(MessageComposerMode.Normal) internal set } 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 2da723746c..15e7d90168 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 @@ -24,10 +24,9 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.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 @@ -45,6 +44,7 @@ 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.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.matrix.api.permalink.PermalinkParser @@ -54,12 +54,10 @@ import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType import io.element.android.libraries.matrix.api.room.getDirectRoomMember import io.element.android.libraries.matrix.api.room.isDm -import io.element.android.libraries.matrix.api.room.roomMembers 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.reply.InReplyToDetails import io.element.android.libraries.matrix.ui.messages.reply.map -import io.element.android.libraries.matrix.ui.room.getDirectRoomMember import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider import io.element.android.libraries.mediaupload.api.MediaSender @@ -99,7 +97,8 @@ import timber.log.Timber import kotlin.time.Duration.Companion.seconds import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes -class MessageComposerPresenter @AssistedInject constructor( +@AssistedInject +class MessageComposerPresenter( @Assisted private val navigator: MessagesNavigator, @Assisted private val timelineController: TimelineController, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, @@ -165,8 +164,8 @@ class MessageComposerPresenter @AssistedInject constructor( val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType -> handlePickedMedia(uri, mimeType) } - val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri -> - handlePickedMedia(uri, MimeTypes.OctetStream) + val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri, mimeType -> + handlePickedMedia(uri, mimeType ?: MimeTypes.OctetStream) } val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri -> handlePickedMedia(uri, MimeTypes.Jpeg) @@ -246,16 +245,23 @@ class MessageComposerPresenter @AssistedInject constructor( richTextEditorState = richTextEditorState, ) } - is MessageComposerEvents.SendUri -> sessionCoroutineScope.sendAttachment( - attachment = Attachment.Media( - localMedia = localMediaFactory.createFromUri( - uri = event.uri, - mimeType = null, - name = null, - formattedFileSize = null + is MessageComposerEvents.SendUri -> { + val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId + sessionCoroutineScope.sendAttachment( + attachment = Attachment.Media( + localMedia = localMediaFactory.createFromUri( + uri = event.uri, + mimeType = null, + name = null, + formattedFileSize = null + ), ), - ), - ) + inReplyToEventId = inReplyToEventId, + ) + + // Reset composer since the attachment has been sent + messageComposerContext.composerMode = MessageComposerMode.Normal + } is MessageComposerEvents.SetMode -> { localCoroutineScope.setMode(event.composerMode, markdownTextEditorState, richTextEditorState) } @@ -496,18 +502,19 @@ class MessageComposerPresenter @AssistedInject constructor( private fun CoroutineScope.sendAttachment( attachment: Attachment, + inReplyToEventId: EventId?, ) = when (attachment) { is Attachment.Media -> { launch { sendMedia( uri = attachment.localMedia.uri, mimeType = attachment.localMedia.info.mimeType, + inReplyToEventId = inReplyToEventId, ) } } } - @UnstableApi private fun handlePickedMedia( uri: Uri?, mimeType: String? = null, @@ -520,17 +527,23 @@ class MessageComposerPresenter @AssistedInject constructor( formattedFileSize = null ) val mediaAttachment = Attachment.Media(localMedia) - navigator.onPreviewAttachment(persistentListOf(mediaAttachment)) + val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId + navigator.onPreviewAttachment(persistentListOf(mediaAttachment), inReplyToEventId) + + // Reset composer since the attachment will be sent in a separate flow + messageComposerContext.composerMode = MessageComposerMode.Normal } private suspend fun sendMedia( uri: Uri, mimeType: String, + inReplyToEventId: EventId?, ) = runCatchingExceptions { mediaSender.sendMedia( uri = uri, mimeType = mimeType, mediaOptimizationConfig = mediaOptimizationConfigProvider.get(), + inReplyToEventId = inReplyToEventId, ).getOrThrow() } .onFailure { cause -> diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/RichTextEditorStateFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/RichTextEditorStateFactory.kt index cc81549458..c22325a9a5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/RichTextEditorStateFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/RichTextEditorStateFactory.kt @@ -8,11 +8,11 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.Composable -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.wysiwyg.compose.RichTextEditorState import io.element.android.wysiwyg.compose.rememberRichTextEditorState -import javax.inject.Inject interface RichTextEditorStateFactory { @Composable @@ -20,7 +20,8 @@ interface RichTextEditorStateFactory { } @ContributesBinding(AppScope::class) -class DefaultRichTextEditorStateFactory @Inject constructor() : RichTextEditorStateFactory { +@Inject +class DefaultRichTextEditorStateFactory : RichTextEditorStateFactory { @Composable override fun remember(): RichTextEditorState { return rememberRichTextEditorState() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/RoomAliasSuggestionsDataSource.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/RoomAliasSuggestionsDataSource.kt index 729bc839fd..5b3a1edf1e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/RoomAliasSuggestionsDataSource.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/RoomAliasSuggestionsDataSource.kt @@ -7,14 +7,14 @@ package io.element.android.features.messages.impl.messagecomposer.suggestions -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.di.SessionScope 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.roomlist.RoomListService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject data class RoomAliasSuggestion( val roomAlias: RoomAlias, @@ -28,7 +28,8 @@ interface RoomAliasSuggestionsDataSource { } @ContributesBinding(SessionScope::class) -class DefaultRoomAliasSuggestionsDataSource @Inject constructor( +@Inject +class DefaultRoomAliasSuggestionsDataSource( private val roomListService: RoomListService, ) : RoomAliasSuggestionsDataSource { override fun getAllRoomAliasSuggestions(): Flow> { 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 99a72e06a2..0f0480f404 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 @@ -155,7 +155,6 @@ internal fun SuggestionsPickerViewPreview() { membership = RoomMembershipState.JOIN, isNameAmbiguous = false, powerLevel = 0L, - normalizedPowerLevel = 0L, isIgnored = false, role = RoomMember.Role.User, membershipChangeReason = null, 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 5055d09a3b..4f008705a5 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 @@ -7,6 +7,7 @@ package io.element.android.features.messages.impl.messagecomposer.suggestions +import dev.zacsweers.metro.Inject 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.RoomMember @@ -16,12 +17,12 @@ import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.SuggestionType -import javax.inject.Inject /** * This class is responsible for processing suggestions when `@`, `/` or `#` are type in the composer. */ -class SuggestionsProcessor @Inject constructor() { +@Inject +class SuggestionsProcessor { /** * Process the suggestion. * @param suggestion The current suggestion input 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 9f18ec86ae..811516e022 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 @@ -7,11 +7,12 @@ package io.element.android.features.messages.impl.pinned +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.sync.SyncService @@ -26,10 +27,10 @@ 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( +@Inject +class PinnedEventsTimelineProvider( private val room: JoinedRoom, private val syncService: SyncService, private val dispatchers: CoroutineDispatchers, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItemFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItemFactory.kt index 55550bde7a..c6e177d87a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItemFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItemFactory.kt @@ -8,13 +8,14 @@ package io.element.android.features.messages.impl.pinned.banner import androidx.compose.ui.text.AnnotatedString +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import kotlinx.coroutines.withContext -import javax.inject.Inject -class PinnedMessagesBannerItemFactory @Inject constructor( +@Inject +class PinnedMessagesBannerItemFactory( private val coroutineDispatchers: CoroutineDispatchers, private val formatter: PinnedMessagesBannerFormatter, ) { 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 21a137b363..5833da56dc 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 @@ -17,6 +17,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -29,9 +30,9 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import javax.inject.Inject -class PinnedMessagesBannerPresenter @Inject constructor( +@Inject +class PinnedMessagesBannerPresenter( private val room: BaseRoom, private val itemFactory: PinnedMessagesBannerItemFactory, private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider, 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 45bed2cc20..8ba776c520 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 @@ -18,9 +18,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode 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 @@ -38,7 +38,8 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.ui.strings.CommonStrings @ContributesNode(RoomScope::class) -class PinnedMessagesListNode @AssistedInject constructor( +@AssistedInject +class PinnedMessagesListNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: PinnedMessagesListPresenter.Factory, 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 68d2e26109..0c7fb8948a 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 @@ -17,9 +17,9 @@ import androidx.compose.runtime.produceState 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.PinUnpinAction import io.element.android.features.messages.impl.UserEventPermissions @@ -61,7 +61,8 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber -class PinnedMessagesListPresenter @AssistedInject constructor( +@AssistedInject +class PinnedMessagesListPresenter( @Assisted private val navigator: PinnedMessagesListNavigator, private val room: JoinedRoom, timelineItemsFactoryCreator: TimelineItemsFactory.Creator, @@ -118,7 +119,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val userEventPermissions by userEventPermissions(syncUpdateFlow.value) - val displayThreadSummaries by featureFlagService.isFeatureEnabledFlow(FeatureFlags.HideThreadedEvents).collectAsState(false) + val displayThreadSummaries by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Threads).collectAsState(false) var pinnedMessageItems by remember { mutableStateOf>>(AsyncData.Uninitialized) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageNode.kt index 027f36df84..e40de20824 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageNode.kt @@ -12,9 +12,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope @@ -22,7 +22,8 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId @ContributesNode(RoomScope::class) -class ReportMessageNode @AssistedInject constructor( +@AssistedInject +class ReportMessageNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: ReportMessagePresenter.Factory, 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 538d9ce25a..90b6585718 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 @@ -15,9 +15,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState @@ -30,7 +30,8 @@ import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class ReportMessagePresenter @AssistedInject constructor( +@AssistedInject +class ReportMessagePresenter( private val room: JoinedRoom, @Assisted private val inputs: Inputs, private val snackbarDispatcher: SnackbarDispatcher, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt index cde141dcd6..f732def95f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt @@ -8,7 +8,6 @@ package io.element.android.features.messages.impl.threads import android.app.Activity -import android.content.Context import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -24,9 +23,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.features.messages.impl.MessagesPresenter @@ -44,12 +43,10 @@ import io.element.android.features.messages.impl.timeline.di.TimelineItemPresent import io.element.android.features.messages.impl.timeline.model.TimelineItem 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 @@ -57,6 +54,7 @@ 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.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.CreateTimelineParams @@ -65,18 +63,18 @@ import io.element.android.libraries.matrix.api.room.alias.matches import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.mediaplayer.api.MediaPlayer -import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @ContributesNode(RoomScope::class) -class ThreadedMessagesNode @AssistedInject constructor( +@AssistedInject +class ThreadedMessagesNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - @ApplicationContext private val context: Context, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val room: JoinedRoom, private val analyticsService: AnalyticsService, @@ -114,7 +112,7 @@ class ThreadedMessagesNode @AssistedInject constructor( interface Callback : Plugin { fun onEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean - fun onPreviewAttachments(attachments: ImmutableList) + fun onPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) fun onUserDataClick(userId: UserId) fun onPermalinkClick(data: PermalinkData) fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) @@ -124,6 +122,7 @@ class ThreadedMessagesNode @AssistedInject constructor( fun onCreatePollClick() fun onEditPollClick(eventId: EventId) fun onJoinCallClick(roomId: RoomId) + fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) } override fun onBuilt() { @@ -190,8 +189,11 @@ class ThreadedMessagesNode @AssistedInject constructor( if (eventId != null) { eventSink(TimelineEvents.FocusOnEvent(eventId)) } else { - // Click on the same room, ignore - displaySameRoomToast() + // Click on the same room, navigate up + // Note that it can not be enough to go back to the room if the thread has been opened + // following a permalink from another thread. In this case navigating up will go back + // to the previous thread. But this should not happen often. + navigateUp() } } else { callbacks.forEach { it.onPermalinkClick(roomLink) } @@ -214,11 +216,18 @@ class ThreadedMessagesNode @AssistedInject constructor( callbacks.forEach { it.onEditPollClick(eventId) } } - override fun onPreviewAttachment(attachments: ImmutableList) { - callbacks.forEach { it.onPreviewAttachments(attachments) } + override fun onPreviewAttachment(attachments: ImmutableList, inReplyToEventId: EventId?) { + callbacks.forEach { it.onPreviewAttachments(attachments, inReplyToEventId) } } - override fun onNavigateToRoom(roomId: RoomId, serverNames: List) = Unit + override fun onNavigateToRoom(roomId: RoomId, eventId: EventId?, serverNames: List) { + val permalinkData = PermalinkData.RoomLink(roomId.toRoomIdOrAlias(), eventId, viaParameters = serverNames.toImmutableList()) + callbacks.forEach { it.onPermalinkClick(permalinkData) } + } + + override fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) { + callbacks.forEach { it.onOpenThread(threadRootId, focusedEventId) } + } private fun onSendLocationClick() { callbacks.forEach { it.onSendLocationClick() } @@ -232,13 +241,6 @@ class ThreadedMessagesNode @AssistedInject constructor( callbacks.forEach { it.onJoinCallClick(room.roomId) } } - private fun displaySameRoomToast() { - context.toast(CommonStrings.screen_room_permalink_same_room_android) - } - - override fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) { - } - @Composable override fun View(modifier: Modifier) { val activity = requireNotNull(LocalActivity.current) @@ -272,11 +274,11 @@ class ThreadedMessagesNode @AssistedInject constructor( onUserDataClick = this::onUserDataClick, onLinkClick = { url, customTab -> onLinkClick( - activity, - isDark, - url, - state.timelineState.eventSink, - customTab + activity = activity, + darkTheme = isDark, + url = url, + eventSink = state.timelineState.eventSink, + customTab = customTab, ) }, onSendLocationClick = this::onSendLocationClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProvider.kt index 78e763fcc2..54c4e55deb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProvider.kt @@ -13,11 +13,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.messages.api.timeline.HtmlConverterProvider import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.di.SingleIn import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.wysiwyg.compose.StyledHtmlConverter @@ -25,11 +26,11 @@ import io.element.android.wysiwyg.display.MentionDisplayHandler import io.element.android.wysiwyg.display.TextDisplay import io.element.android.wysiwyg.utils.HtmlConverter import uniffi.wysiwyg_composer.newMentionDetector -import javax.inject.Inject @ContributesBinding(RoomScope::class) @SingleIn(RoomScope::class) -class DefaultHtmlConverterProvider @Inject constructor( +@Inject +class DefaultHtmlConverterProvider( private val mentionSpanProvider: MentionSpanProvider, ) : HtmlConverterProvider { private val htmlConverter: MutableState = mutableStateOf(null) 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 index b2962e2f5a..6fe1cd687c 100644 --- 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 @@ -7,21 +7,22 @@ package io.element.android.features.messages.impl.timeline -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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( +@Inject +class DefaultMarkAsFullyRead( private val matrixClient: MatrixClient, ) : MarkAsFullyRead { override fun invoke(roomId: RoomId) { 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 6289feecfd..779ebe984a 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 @@ -7,11 +7,14 @@ package io.element.android.features.messages.impl.timeline -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metro.binding import io.element.android.features.messages.impl.timeline.di.LiveTimeline 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.ThreadId import io.element.android.libraries.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem @@ -34,15 +37,15 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import java.io.Closeable import java.util.Optional -import javax.inject.Inject /** * This controller is responsible of using the right timeline to display messages and make associated actions. * It can be focused on the live timeline or on a detached timeline (focusing an unknown event). */ @SingleIn(RoomScope::class) -@ContributesBinding(RoomScope::class, boundType = TimelineProvider::class) -class TimelineController @Inject constructor( +@ContributesBinding(RoomScope::class, binding = binding()) +@Inject +class TimelineController( private val room: JoinedRoom, @LiveTimeline private val liveTimeline: Timeline, ) : Closeable, TimelineProvider { @@ -72,21 +75,26 @@ class TimelineController @Inject constructor( } } - suspend fun focusOnEvent(eventId: EventId): Result { - return room.createTimeline(CreateTimelineParams.Focused(eventId)) - .onFailure { - if (it is CancellationException) { - throw it - } - } - .map { newDetachedTimeline -> - detachedTimelineFlow.getAndUpdate { current -> - if (current.isPresent) { - current.get().close() + suspend fun focusOnEvent(eventId: EventId, threadRootId: ThreadId?): Result { + return if (threadRootId != null) { + Result.success(EventFocusResult.IsInThread(threadRootId)) + } else { + room.createTimeline(CreateTimelineParams.Focused(eventId)) + .onFailure { + if (it is CancellationException) { + throw it } - Optional.of(newDetachedTimeline) } - } + .map { newDetachedTimeline -> + detachedTimelineFlow.getAndUpdate { current -> + if (current.isPresent) { + current.get().close() + } + Optional.of(newDetachedTimeline) + } + EventFocusResult.FocusedOnLive + } + } } /** @@ -134,3 +142,8 @@ class TimelineController @Inject constructor( return currentTimelineFlow } } + +sealed interface EventFocusResult { + data object FocusedOnLive : EventFocusResult + data class IsInThread(val threadId: ThreadId) : EventFocusResult +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineItemIndexer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineItemIndexer.kt index 884f0964bb..36b64be0ce 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineItemIndexer.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineItemIndexer.kt @@ -7,15 +7,16 @@ package io.element.android.features.messages.impl.timeline +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.matrix.api.core.EventId import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber -import javax.inject.Inject -class TimelineItemIndexer @Inject constructor() { +@Inject +class TimelineItemIndexer { // This is a latch to wait for the first process call private val firstProcessLatch = CompletableDeferred() private val timelineEventsIndexes = mutableMapOf() 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 32556eae7d..f03a1e8903 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 @@ -20,9 +20,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureEvents import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState @@ -44,6 +44,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId +import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.isDm @@ -67,7 +68,8 @@ import timber.log.Timber const val FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS = 200L -class TimelinePresenter @AssistedInject constructor( +@AssistedInject +class TimelinePresenter( timelineItemsFactoryCreator: TimelineItemsFactory.Creator, private val room: JoinedRoom, private val dispatchers: CoroutineDispatchers, @@ -116,8 +118,8 @@ class TimelinePresenter @AssistedInject constructor( val syncUpdateFlow = room.syncUpdateFlow.collectAsState() - val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value) - val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION, updateKey = syncUpdateFlow.value) + val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.RoomMessage, updateKey = syncUpdateFlow.value) + val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.Reaction, updateKey = syncUpdateFlow.value) val prevMostRecentItemId = rememberSaveable { mutableStateOf(null) } @@ -136,7 +138,7 @@ class TimelinePresenter @AssistedInject constructor( }.collectAsState(initial = true) val displayThreadSummaries by produceState(false) { - value = featureFlagService.isFeatureEnabled(FeatureFlags.HideThreadedEvents) + value = featureFlagService.isFeatureEnabled(FeatureFlags.Threads) } fun handleEvents(event: TimelineEvents) { @@ -206,7 +208,7 @@ class TimelinePresenter @AssistedInject constructor( is TimelineEvents.NavigateToPredecessorOrSuccessorRoom -> { // Navigate to the predecessor or successor room val serverNames = calculateServerNamesForRoom(room) - navigator.onNavigateToRoom(event.roomId, serverNames) + navigator.onNavigateToRoom(event.roomId, null, serverNames) } is TimelineEvents.OpenThread -> { navigator.onOpenThread( @@ -256,13 +258,39 @@ class TimelinePresenter @AssistedInject constructor( } is FocusRequestState.Loading -> { val eventId = currentFocusRequestState.eventId - timelineController.focusOnEvent(eventId) - .onSuccess { - focusRequestState = FocusRequestState.Success(eventId = eventId) - } - .onFailure { - focusRequestState = FocusRequestState.Failure(it) - } + val threadId = room.threadRootIdForEvent(eventId).getOrElse { + focusRequestState = FocusRequestState.Failure(it) + return@LaunchedEffect + } + + if (timelineController.mainTimelineMode() is Timeline.Mode.Thread && threadId == null) { + // We are in a thread timeline, and the event isn't part of a thread, we need to navigate back to the room + focusRequestState = FocusRequestState.None + navigator.onNavigateToRoom(room.roomId, eventId, calculateServerNamesForRoom(room)) + } else { + timelineController.focusOnEvent(eventId, threadId) + .onSuccess { result -> + when (result) { + is EventFocusResult.FocusedOnLive -> { + focusRequestState = FocusRequestState.Success(eventId = eventId) + } + is EventFocusResult.IsInThread -> { + val currentThreadId = (timelineController.mainTimelineMode() as? Timeline.Mode.Thread)?.threadRootId + if (currentThreadId == result.threadId) { + // It's the same thread, we just focus on the event + focusRequestState = FocusRequestState.Success(eventId = eventId) + } else { + focusRequestState = FocusRequestState.Success(eventId = result.threadId.asEventId()) + // It's part of a thread we're not in, let's open it in another timeline + navigator.onOpenThread(result.threadId, eventId) + } + } + } + } + .onFailure { + focusRequestState = FocusRequestState.Failure(it) + } + } } else -> Unit } @@ -340,7 +368,7 @@ class TimelinePresenter @AssistedInject constructor( newMostRecentItemId != prevMostRecentItemIdValue if (hasNewEvent) { - val newMostRecentEvent = newMostRecentItem as? TimelineItem.Event + val newMostRecentEvent = newMostRecentItem // Scroll to bottom if the new event is from me, even if sent from another device val fromMe = newMostRecentEvent?.isMine == true newEventState.value = if (fromMe) { 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 3c03f4a7b0..e7dc71f185 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 @@ -16,6 +16,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts +import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo import io.element.android.features.messages.impl.timeline.model.anAggregatedReaction import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent @@ -32,7 +33,6 @@ 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.Timeline -import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo 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 @@ -146,7 +146,7 @@ internal fun aTimelineItemEvent( groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, sendState: LocalEventSendState? = null, inReplyTo: InReplyToDetails? = null, - threadInfo: EventThreadInfo = EventThreadInfo(threadRootId = null, threadSummary = null), + threadInfo: TimelineItemThreadInfo? = null, debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(), readReceiptState: TimelineItemReadReceipts = aTimelineItemReadReceipts(), 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 04fe0cb481..1dbe5d3a24 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 @@ -53,8 +53,6 @@ import io.element.android.libraries.ui.utils.time.isTalkbackActive private val BUBBLE_RADIUS = 12.dp private val avatarRadius = AvatarSize.TimelineSender.dp / 2 -// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 78% now. -private const val BUBBLE_WIDTH_RATIO = 0.78f private val MIN_BUBBLE_WIDTH = 80.dp @Composable @@ -66,34 +64,6 @@ fun MessageEventBubble( modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit = {}, ) { - fun bubbleShape(): Shape { - val topLeftCorner = if (state.cutTopStart) 0.dp else BUBBLE_RADIUS - return when (state.groupPosition) { - TimelineItemGroupPosition.First -> if (state.isMine) { - RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS) - } else { - RoundedCornerShape(topLeftCorner, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp) - } - TimelineItemGroupPosition.Middle -> if (state.isMine) { - RoundedCornerShape(BUBBLE_RADIUS, 0.dp, 0.dp, BUBBLE_RADIUS) - } else { - RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp) - } - TimelineItemGroupPosition.Last -> if (state.isMine) { - RoundedCornerShape(BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS) - } else { - RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, BUBBLE_RADIUS) - } - TimelineItemGroupPosition.None -> - RoundedCornerShape( - topLeftCorner, - BUBBLE_RADIUS, - BUBBLE_RADIUS, - BUBBLE_RADIUS - ) - } - } - val clickableModifier = if (isTalkbackActive()) { Modifier } else { @@ -108,11 +78,8 @@ fun MessageEventBubble( } // Ignore state.isHighlighted for now, we need a design decision on it. - val backgroundBubbleColor = when { - state.isMine -> ElementTheme.colors.messageFromMeBackground - else -> ElementTheme.colors.messageFromOtherBackground - } - val bubbleShape = bubbleShape() + val backgroundBubbleColor = MessageEventBubbleDefaults.backgroundBubbleColor(state.isMine) + val bubbleShape = remember(state) { MessageEventBubbleDefaults.shape(state.cutTopStart, state.groupPosition, state.isMine) } val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx() val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx() val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl @@ -147,7 +114,7 @@ fun MessageEventBubble( .testTag(TestTags.messageBubble) .widthIn( min = MIN_BUBBLE_WIDTH, - max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO) + max = (constraints.maxWidth * MessageEventBubbleDefaults.BUBBLE_WIDTH_RATIO) .toInt() .toDp() ) @@ -157,6 +124,48 @@ fun MessageEventBubble( } } +object MessageEventBubbleDefaults { + fun shape(cutTopStart: Boolean, groupPosition: TimelineItemGroupPosition, isMine: Boolean): Shape { + val topLeftCorner = if (cutTopStart) 0.dp else BUBBLE_RADIUS + return when (groupPosition) { + TimelineItemGroupPosition.First -> if (isMine) { + RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS) + } else { + RoundedCornerShape(topLeftCorner, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp) + } + TimelineItemGroupPosition.Middle -> if (isMine) { + RoundedCornerShape(BUBBLE_RADIUS, 0.dp, 0.dp, BUBBLE_RADIUS) + } else { + RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp) + } + TimelineItemGroupPosition.Last -> if (isMine) { + RoundedCornerShape(BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS) + } else { + RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, BUBBLE_RADIUS) + } + TimelineItemGroupPosition.None -> + RoundedCornerShape( + topLeftCorner, + BUBBLE_RADIUS, + BUBBLE_RADIUS, + BUBBLE_RADIUS + ) + } + } + + @Composable + fun backgroundBubbleColor(isMine: Boolean): Color { + return if (isMine) { + ElementTheme.colors.messageFromMeBackground + } else { + ElementTheme.colors.messageFromOtherBackground + } + } + + // Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 78% now. + const val BUBBLE_WIDTH_RATIO = 0.78f +} + @PreviewsDayNight @Composable internal fun MessageEventBubblePreview(@PreviewParameter(BubbleStateProvider::class) state: BubbleState) = ElementPreview { 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 6cbd22c3fa..aaebfb4957 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 @@ -29,7 +29,7 @@ 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.model.TimelineItem -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.api.RoomCallStateProvider import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -119,7 +119,7 @@ internal fun TimelineItemCallNotifyViewPreview() = ElementPreview { .filter { it !is RoomCallState.Unavailable } .forEach { roomCallState -> TimelineItemCallNotifyView( - event = aTimelineItemEvent(content = TimelineItemCallNotifyContent()), + event = aTimelineItemEvent(content = TimelineItemRtcNotificationContent()), roomCallState = roomCallState, onLongClick = {}, onJoinCallClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index de4cfe2c2a..1503bf236b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -23,6 +24,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -34,6 +37,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.res.pluralStringResource @@ -43,6 +47,7 @@ import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.isTraversalGroup import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.traversalIndex +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp @@ -61,6 +66,7 @@ import io.element.android.features.messages.impl.timeline.components.receipt.Rea import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition +import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent @@ -78,25 +84,28 @@ import io.element.android.libraries.designsystem.colors.AvatarColorsProvider import io.element.android.libraries.designsystem.components.EqualWidthColumn 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.swipe.SwipeableActionsState import io.element.android.libraries.designsystem.swipe.rememberSwipeableActionsState import io.element.android.libraries.designsystem.text.toPx -import io.element.android.libraries.designsystem.theme.components.Button -import io.element.android.libraries.designsystem.theme.components.ButtonSize import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.toThreadId import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo +import io.element.android.libraries.matrix.api.timeline.item.EmbeddedEventInfo import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary +import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId +import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails +import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl +import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails @@ -256,22 +265,22 @@ fun TimelineItemEventRow( ) } - if (displayThreadSummaries && timelineMode !is Timeline.Mode.Thread) { - event.threadInfo.threadSummary?.let { threadSummary -> - val threadPart = stringResource(CommonStrings.common_thread) - val numberOfReplies = threadSummary.numberOfReplies.toInt().let { replies -> - pluralStringResource(CommonPlurals.common_replies, replies, replies) + if (displayThreadSummaries && timelineMode !is Timeline.Mode.Thread && event.threadInfo is TimelineItemThreadInfo.ThreadRoot) { + ThreadSummaryView( + modifier = if (event.isMine) { + Modifier.align(Alignment.End).padding(end = 16.dp) + } else { + if (timelineRoomInfo.isDm) Modifier else Modifier.padding(start = 16.dp) + }.padding(top = 2.dp), + threadSummary = event.threadInfo.summary, + latestEventText = event.threadInfo.latestEventText, + isOutgoing = event.isMine, + onClick = { + event.eventId?.let { + eventSink(TimelineEvents.OpenThread(it.toThreadId(), null)) + } } - Button( - modifier = Modifier.padding(horizontal = 24.dp, vertical = 2.dp) - .align(if (event.isMine) Alignment.End else Alignment.Start), - text = "$threadPart - $numberOfReplies", - size = ButtonSize.Small, - onClick = { - eventSink(TimelineEvents.OpenThread(event.eventId!!.toThreadId(), null)) - }, - ) - } + ) } // Read receipts / Send state @@ -288,6 +297,81 @@ fun TimelineItemEventRow( } } +@Composable +private fun ThreadSummaryView( + threadSummary: ThreadSummary, + latestEventText: String?, + isOutgoing: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithConstraints(modifier = modifier) { + Row( + modifier = Modifier + .then(if (!isOutgoing) Modifier.padding(start = 16.dp) else Modifier) + .graphicsLayer { + shape = RoundedCornerShape(8.dp) + clip = true + } + .background(MessageEventBubbleDefaults.backgroundBubbleColor(isOutgoing)) + .niceClickable(onClick) + .padding(horizontal = 12.dp, vertical = 10.dp) + .widthIn(max = (maxWidth - 24.dp) * MessageEventBubbleDefaults.BUBBLE_WIDTH_RATIO), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = CompoundIcons.ThreadsSolid(), + contentDescription = null, + tint = ElementTheme.colors.iconSecondary, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = pluralStringResource(CommonPlurals.common_replies, threadSummary.numberOfReplies.toInt(), threadSummary.numberOfReplies), + style = ElementTheme.typography.fontBodySmMedium, + color = ElementTheme.colors.textSecondary, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + threadSummary.latestEvent.dataOrNull()?.let { latestEvent -> + val avatarData = AvatarData( + id = latestEvent.senderId.value, + name = latestEvent.senderProfile.getDisplayName(), + url = latestEvent.senderProfile.getAvatarUrl(), + size = AvatarSize.TimelineThreadLatestEventSender, + ) + Avatar( + avatarData = avatarData, + avatarType = AvatarType.User, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = latestEvent.senderProfile.getDisambiguatedDisplayName(latestEvent.senderId), + style = ElementTheme.typography.fontBodySmMedium, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Spacer(modifier = Modifier.width(4.dp)) + + latestEventText?.let { + Text( + text = it, + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + } + } + } +} + /** * Impact ViewConfiguration.touchSlop by [sensitivityFactor]. * Inspired from https://issuetracker.google.com/u/1/issues/269627294. @@ -694,7 +778,7 @@ private fun MessageEventBubbleContent( else -> ContentPadding.Textual } CommonLayout( - showThreadDecoration = timelineMode !is Timeline.Mode.Thread && event.threadInfo.threadRootId != null, + showThreadDecoration = timelineMode !is Timeline.Mode.Thread && event.threadInfo is TimelineItemThreadInfo.ThreadResponse, timestampPosition = timestampPosition, paddingBehaviour = paddingBehaviour, inReplyToDetails = event.inReplyTo, @@ -746,9 +830,27 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview { " hopefully can be manually adjusted to test different behaviors." ), groupPosition = TimelineItemGroupPosition.First, - threadInfo = EventThreadInfo( - threadRootId = ThreadId("\$thread-root-id"), - threadSummary = ThreadSummary(AsyncData.Uninitialized, numberOfReplies = 20L) + threadInfo = TimelineItemThreadInfo.ThreadRoot( + latestEventText = "This is the latest message in the thread", + summary = ThreadSummary(AsyncData.Success( + EmbeddedEventInfo( + eventOrTransactionId = EventOrTransactionId.Event(EventId("\$event-id")), + content = MessageContent( + body = "This is the latest message in the thread", + inReplyTo = null, + isEdited = false, + threadInfo = null, + type = TextMessageType("This is the latest message in the thread", null) + ), + senderId = UserId("@user:id"), + senderProfile = ProfileTimelineDetails.Ready( + displayName = "Alice", + avatarUrl = null, + displayNameAmbiguous = false, + ), + timestamp = 0L, + ) + ), numberOfReplies = 20L) ) ), displayThreadSummaries = true, @@ -756,3 +858,40 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview { } } } + +@PreviewsDayNight +@Composable +internal fun ThreadSummaryViewPreview() { + ElementPreview { + val body = "This is the latest message in the thread" + val threadSummary = ThreadSummary( + AsyncData.Success( + EmbeddedEventInfo( + eventOrTransactionId = EventOrTransactionId.Event(EventId("\$event-id")), + content = MessageContent( + body = body, + inReplyTo = null, + isEdited = false, + threadInfo = null, + type = TextMessageType(body, null) + ), + senderId = UserId("@user:id"), + senderProfile = ProfileTimelineDetails.Ready( + displayName = "Alice", + avatarUrl = null, + displayNameAmbiguous = true, + ), + timestamp = 0L, + ) + ), + numberOfReplies = 12, + ) + + ThreadSummaryView( + threadSummary = threadSummary, + latestEventText = "Some event with a very long text that should get clipped", + isOutgoing = true, + onClick = {}, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt index 61e86a8d59..7cc1843466 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt @@ -13,12 +13,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition +import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.core.ThreadId -import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsProvider @@ -58,10 +58,7 @@ internal fun TimelineItemEventRowWithReplyContentToPreview( ), inReplyTo = inReplyToDetails, displayNameAmbiguous = displayNameAmbiguous, - threadInfo = EventThreadInfo( - threadRootId = ThreadId("\$thread-root-id"), - threadSummary = null, - ), + threadInfo = TimelineItemThreadInfo.ThreadResponse(threadRootId = ThreadId("\$thread-root-id")), groupPosition = TimelineItemGroupPosition.Last, ), ) 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 0ce8e02ecc..1a597fbda6 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 @@ -74,7 +74,7 @@ fun TimelineItemGroupedEventsRow( ) }, ) { - val isExpanded = rememberSaveable(key = timelineItem.identifier().value) { mutableStateOf(false) } + val isExpanded = rememberSaveable { mutableStateOf(false) } fun onExpandGroupClick() { isExpanded.value = !isExpanded.value 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 11d7b91e1e..119a235cf8 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 @@ -30,9 +30,9 @@ import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData 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.TimelineItemRtcNotificationContent 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 @@ -123,7 +123,7 @@ internal fun TimelineItemRow( eventSink = eventSink, ) } - is TimelineItemCallNotifyContent -> { + is TimelineItemRtcNotificationContent -> { TimelineItemCallNotifyView( modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp), event = timelineItem, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt index b2bd0de280..d0c848e393 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt @@ -11,9 +11,13 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import io.element.android.emojibasebindings.Emoji +import io.element.android.features.messages.impl.timeline.components.customreaction.picker.EmojiPicker +import io.element.android.features.messages.impl.timeline.components.customreaction.picker.EmojiPickerPresenter +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.hide import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId @@ -47,9 +51,16 @@ fun CustomReactionBottomSheet( sheetState = sheetState, modifier = modifier ) { + val presenter = remember { + EmojiPickerPresenter( + emojibaseStore = target.emojibaseStore, + recentEmojis = state.recentEmojis, + coroutineDispatchers = CoroutineDispatchers.Default, + ) + } EmojiPicker( onSelectEmoji = ::onEmojiSelectedDismiss, - emojibaseStore = target.emojibaseStore, + state = presenter.present(), selectedEmojis = state.selectedEmoji, modifier = Modifier.fillMaxSize(), ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt index 028305d86e..ba13c461e4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt @@ -9,28 +9,39 @@ package io.element.android.features.messages.impl.timeline.components.customreac 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.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.recentemojis.GetRecentEmojis +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.launch -import javax.inject.Inject -class CustomReactionPresenter @Inject constructor( - private val emojibaseProvider: EmojibaseProvider +@Inject +class CustomReactionPresenter( + private val emojibaseProvider: EmojibaseProvider, + private val getRecentEmojis: GetRecentEmojis, ) : Presenter { @Composable override fun present(): CustomReactionState { + val localCoroutineScope = rememberCoroutineScope() + var recentEmojis by remember { mutableStateOf>(persistentListOf()) } + val target: MutableState = remember { mutableStateOf(CustomReactionState.Target.None) } - val localCoroutineScope = rememberCoroutineScope() fun handleShowCustomReactionSheet(event: TimelineItem.Event) { target.value = CustomReactionState.Target.Loading(event) localCoroutineScope.launch { + recentEmojis = getRecentEmojis().getOrNull().orEmpty().toImmutableList() target.value = CustomReactionState.Target.Success( event = event, emojibaseStore = emojibaseProvider.emojibaseStore @@ -55,9 +66,11 @@ class CustomReactionPresenter @Inject constructor( ?.mapNotNull { if (it.isHighlighted) it.key else null } .orEmpty() .toImmutableSet() + return CustomReactionState( target = target.value, selectedEmoji = selectedEmoji, + recentEmojis = recentEmojis, eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt index 61fb0d7dde..9a9a985e62 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt @@ -9,11 +9,13 @@ package io.element.android.features.messages.impl.timeline.components.customreac import io.element.android.emojibasebindings.EmojibaseStore import io.element.android.features.messages.impl.timeline.model.TimelineItem +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet data class CustomReactionState( val target: Target, val selectedEmoji: ImmutableSet, + val recentEmojis: ImmutableList, val eventSink: (CustomReactionEvents) -> Unit, ) { sealed interface Target { 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 b6ad695aa8..360cb9756e 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 @@ -34,6 +34,7 @@ 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 kotlinx.collections.immutable.persistentListOf @Composable fun EmojiItem( @@ -86,7 +87,7 @@ internal fun EmojiItemPreview() = ElementPreview { hexcode = "", label = "", tags = null, - shortcodes = emptyList(), + shortcodes = persistentListOf(), unicode = "👍", skins = null ), 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 deleted file mode 100644 index fcaac65e82..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt +++ /dev/null @@ -1,110 +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.messages.impl.timeline.components.customreaction - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SecondaryTabRow -import androidx.compose.material3.Tab -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import io.element.android.emojibasebindings.Emoji -import io.element.android.emojibasebindings.EmojibaseCategory -import io.element.android.emojibasebindings.EmojibaseDatasource -import io.element.android.emojibasebindings.EmojibaseStore -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 kotlinx.collections.immutable.ImmutableSet -import kotlinx.collections.immutable.persistentSetOf -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun EmojiPicker( - onSelectEmoji: (Emoji) -> Unit, - emojibaseStore: EmojibaseStore, - selectedEmojis: ImmutableSet, - modifier: Modifier = Modifier, -) { - val coroutineScope = rememberCoroutineScope() - val categories = remember { emojibaseStore.categories } - val pagerState = rememberPagerState(pageCount = { EmojibaseCategory.entries.size }) - Column(modifier) { - SecondaryTabRow( - selectedTabIndex = pagerState.currentPage, - ) { - EmojibaseCategory.entries.forEachIndexed { index, category -> - Tab( - icon = { - Icon( - imageVector = category.icon, - contentDescription = stringResource(id = category.title) - ) - }, - selected = pagerState.currentPage == index, - onClick = { - coroutineScope.launch { pagerState.animateScrollToPage(index) } - } - ) - } - } - - HorizontalPager( - state = pagerState, - modifier = Modifier.fillMaxWidth(), - ) { index -> - val category = EmojibaseCategory.entries[index] - val emojis = categories[category] ?: listOf() - LazyVerticalGrid( - modifier = Modifier.fillMaxSize(), - columns = GridCells.Adaptive(minSize = 48.dp), - contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - items(emojis, key = { it.unicode }) { item -> - EmojiItem( - modifier = Modifier.aspectRatio(1f), - item = item, - isSelected = selectedEmojis.contains(item.unicode), - onSelectEmoji = onSelectEmoji, - emojiSize = 32.dp.toSp(), - ) - } - } - } - } -} - -@PreviewsDayNight -@Composable -internal fun EmojiPickerPreview() = ElementPreview { - EmojiPicker( - onSelectEmoji = {}, - emojibaseStore = EmojibaseDatasource().load(LocalContext.current), - selectedEmojis = persistentSetOf("😀", "😄", "😃"), - modifier = Modifier.fillMaxWidth(), - ) -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt new file mode 100644 index 0000000000..83b21df092 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt @@ -0,0 +1,150 @@ +/* + * 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.messages.impl.timeline.components.customreaction.picker + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SecondaryTabRow +import androidx.compose.material3.Tab +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +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.emojibasebindings.Emoji +import io.element.android.features.messages.impl.timeline.components.customreaction.EmojiItem +import io.element.android.features.messages.impl.timeline.components.customreaction.icon +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.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.SearchBar +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EmojiPicker( + onSelectEmoji: (Emoji) -> Unit, + state: EmojiPickerState, + selectedEmojis: ImmutableSet, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val pagerState = rememberPagerState(pageCount = { state.categories.size }) + Column(modifier) { + SearchBar( + modifier = Modifier.padding(bottom = 10.dp), + query = state.searchQuery, + onQueryChange = { state.eventSink(EmojiPickerEvents.UpdateSearchQuery(it)) }, + resultState = state.searchResults, + active = state.isSearchActive, + onActiveChange = { state.eventSink(EmojiPickerEvents.ToggleSearchActive(it)) }, + windowInsets = WindowInsets(0, 0, 0, 0), + placeHolderTitle = stringResource(CommonStrings.emoji_picker_search_placeholder), + ) { emojis -> + EmojiResults( + emojis = emojis, + isEmojiSelected = { selectedEmojis.contains(it.unicode) }, + onSelectEmoji = onSelectEmoji, + ) + } + + if (!state.isSearchActive) { + SecondaryTabRow( + selectedTabIndex = pagerState.currentPage, + ) { + state.categories.forEachIndexed { index, category -> + Tab( + icon = { + when (category.icon) { + is IconSource.Resource -> Icon( + resourceId = category.icon.id, + contentDescription = stringResource(id = category.titleId) + ) + is IconSource.Vector -> Icon( + imageVector = category.icon.vector, + contentDescription = stringResource(id = category.titleId) + ) + } + }, + selected = pagerState.currentPage == index, + onClick = { + coroutineScope.launch { pagerState.animateScrollToPage(index) } + } + ) + } + } + + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxWidth(), + ) { index -> + val emojis = state.categories[index].emojis + EmojiResults( + emojis = emojis, + isEmojiSelected = { selectedEmojis.contains(it.unicode) }, + onSelectEmoji = onSelectEmoji, + ) + } + } + } +} + +@Composable +private fun EmojiResults( + emojis: ImmutableList, + isEmojiSelected: (Emoji) -> Boolean, + onSelectEmoji: (Emoji) -> Unit, +) { + LazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + columns = GridCells.Adaptive(minSize = 48.dp), + contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + items(emojis, key = { it.unicode }) { item -> + EmojiItem( + modifier = Modifier.aspectRatio(1f), + item = item, + isSelected = isEmojiSelected(item), + onSelectEmoji = onSelectEmoji, + emojiSize = 32.dp.toSp(), + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun EmojiPickerPreview(@PreviewParameter(EmojiPickerStateProvider::class) state: EmojiPickerState) = ElementPreview { + EmojiPicker( + onSelectEmoji = {}, + state = state, + selectedEmojis = persistentSetOf("😀", "😄", "😃"), + modifier = Modifier.fillMaxWidth(), + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerEvents.kt new file mode 100644 index 0000000000..53fa6b7b7a --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerEvents.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.messages.impl.timeline.components.customreaction.picker + +sealed interface EmojiPickerEvents { + data class ToggleSearchActive(val isActive: Boolean) : EmojiPickerEvents + data class UpdateSearchQuery(val query: String) : EmojiPickerEvents +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt new file mode 100644 index 0000000000..ce9600b1f7 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.components.customreaction.picker + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalInspectionMode +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.emojibasebindings.Emoji +import io.element.android.emojibasebindings.EmojibaseStore +import io.element.android.features.messages.impl.R +import io.element.android.features.messages.impl.timeline.components.customreaction.icon +import io.element.android.features.messages.impl.timeline.components.customreaction.title +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import kotlin.time.Duration.Companion.milliseconds + +class EmojiPickerPresenter( + private val emojibaseStore: EmojibaseStore, + private val recentEmojis: ImmutableList, + private val coroutineDispatchers: CoroutineDispatchers, +) : Presenter { + @Composable + override fun present(): EmojiPickerState { + var searchQuery by remember { mutableStateOf("") } + var isSearchActive by remember { mutableStateOf(false) } + var emojiResults by remember { mutableStateOf>>(SearchBarResultState.Initial()) } + + val recentEmojiIcon = CompoundIcons.History() + val categories = remember { + val providedCategories = emojibaseStore.categories.map { (category, emojis) -> + EmojiCategory( + titleId = category.title, + icon = IconSource.Vector(category.icon), + emojis = emojis + ) + } + if (recentEmojis.isNotEmpty()) { + val recentEmojis = recentEmojis.mapNotNull { recentEmoji -> + emojibaseStore.allEmojis.find { it.unicode == recentEmoji } + }.toImmutableList() + val recentCategory = + EmojiCategory( + titleId = R.string.emoji_picker_category_recent, + icon = IconSource.Vector(recentEmojiIcon), + emojis = recentEmojis + ) + (listOf(recentCategory) + providedCategories).toImmutableList() + } else { + providedCategories.toImmutableList() + } + } + + LaunchedEffect(searchQuery) { + emojiResults = if (searchQuery.isEmpty()) { + SearchBarResultState.Initial() + } else { + // Add a small delay to avoid doing too many computations when the user is typing quickly + delay(100.milliseconds) + + val lowercaseQuery = searchQuery.lowercase() + val results = withContext(coroutineDispatchers.computation) { + emojibaseStore.allEmojis + .asSequence() + .filter { emoji -> + emoji.tags.orEmpty().any { it.contains(lowercaseQuery) } || + emoji.shortcodes.any { it.contains(lowercaseQuery) } + } + .take(60) + .toImmutableList() + } + + SearchBarResultState.Results(results) + } + } + + val isInPreview = LocalInspectionMode.current + fun handleEvents(event: EmojiPickerEvents) { + when (event) { + // For some reason, in preview mode the SearchBar emits this event with an `isActive = true` value automatically + is EmojiPickerEvents.ToggleSearchActive -> if (!isInPreview) { + isSearchActive = event.isActive + } + is EmojiPickerEvents.UpdateSearchQuery -> searchQuery = event.query + } + } + + return EmojiPickerState( + categories = categories, + allEmojis = emojibaseStore.allEmojis, + searchQuery = searchQuery, + isSearchActive = isSearchActive, + searchResults = emojiResults, + eventSink = ::handleEvents, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.kt new file mode 100644 index 0000000000..595349a503 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.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.features.messages.impl.timeline.components.customreaction.picker + +import androidx.annotation.StringRes +import io.element.android.emojibasebindings.Emoji +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import kotlinx.collections.immutable.ImmutableList + +data class EmojiPickerState( + val categories: ImmutableList, + val allEmojis: ImmutableList, + val searchQuery: String, + val isSearchActive: Boolean, + val searchResults: SearchBarResultState>, + val eventSink: (EmojiPickerEvents) -> Unit, +) + +/** + * Represents a category of emojis with a title id, icon, and the list of associated emojis. + */ +data class EmojiCategory( + @StringRes val titleId: Int, + val icon: IconSource, + val emojis: ImmutableList, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerStateProvider.kt new file mode 100644 index 0000000000..f248efe893 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerStateProvider.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.android.features.messages.impl.timeline.components.customreaction.picker + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.emojibasebindings.Emoji +import io.element.android.emojibasebindings.EmojibaseCategory +import io.element.android.features.messages.impl.R +import io.element.android.features.messages.impl.timeline.components.customreaction.icon +import io.element.android.features.messages.impl.timeline.components.customreaction.title +import io.element.android.libraries.designsystem.icons.CompoundDrawables +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +class EmojiPickerStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + anEmojiPickerState(), + anEmojiPickerState(isSearchActive = true), + anEmojiPickerState(isSearchActive = true, searchQuery = "smile"), + anEmojiPickerState( + isSearchActive = true, + searchQuery = "smile", + searchResults = SearchBarResultState.Results(emojiList()) + ), + ) +} + +private fun recentEmojisCategory() = EmojiCategory( + titleId = R.string.emoji_picker_category_recent, + icon = IconSource.Resource(CompoundDrawables.ic_compound_history), + emojis = emojiList(), +) + +private fun emojiList(): ImmutableList = persistentListOf( + Emoji( + "0x00", + "grinning face", + persistentListOf("grinning"), + persistentListOf("smile, grin"), + "😀", + null + ), + Emoji( + "0x01", + "crying face", + persistentListOf("crying"), + persistentListOf("smile, crying"), + "\uD83E\uDD72", + null + ) +) + +internal fun anEmojiPickerState( + categories: ImmutableList = (listOf(recentEmojisCategory()) + EmojibaseCategory.entries.map { + EmojiCategory( + titleId = it.title, + icon = IconSource.Vector(it.icon), + emojis = emojiList(), + ) + }).toImmutableList(), + allEmojis: ImmutableList = categories.flatMap { it.emojis }.toImmutableList(), + searchQuery: String = "", + isSearchActive: Boolean = false, + searchResults: SearchBarResultState> = SearchBarResultState.Initial(), + eventSink: (EmojiPickerEvents) -> Unit = {}, +) = EmojiPickerState( + categories = categories, + allEmojis = allEmojis, + searchQuery = searchQuery, + isSearchActive = isSearchActive, + searchResults = searchResults, + eventSink = eventSink, +) 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 332f58777c..8660d82e7c 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 @@ -14,7 +14,6 @@ import io.element.android.features.messages.impl.timeline.components.layout.Cont import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.rememberPresenter import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent @@ -23,6 +22,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent @@ -133,6 +133,6 @@ fun TimelineItemEventContentView( modifier = modifier ) } - is TimelineItemCallNotifyContent -> error("This shouldn't be rendered as the content of a bubble") + is TimelineItemRtcNotificationContent -> error("This shouldn't be rendered as the content of a bubble") } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt index a5c7eb89be..4354ef5b25 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 @@ -14,6 +14,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.RoomMember @@ -21,9 +22,9 @@ import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import javax.inject.Inject -class ReactionSummaryPresenter @Inject constructor( +@Inject +class ReactionSummaryPresenter( private val room: BaseRoom, ) : Presenter { @Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetPresenter.kt index 80092faa2b..33316a134a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetPresenter.kt @@ -12,11 +12,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.architecture.Presenter -import javax.inject.Inject -class ReadReceiptBottomSheetPresenter @Inject constructor() : Presenter { +@Inject +class ReadReceiptBottomSheetPresenter : Presenter { @Composable override fun present(): ReadReceiptBottomSheetState { var selectedEvent: TimelineItem.Event? by remember { mutableStateOf(null) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt index 2a4f193fe9..d9e85c2eb2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt @@ -12,9 +12,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope @@ -22,7 +22,8 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo @ContributesNode(RoomScope::class) -class EventDebugInfoNode @AssistedInject constructor( +@AssistedInject +class EventDebugInfoNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, ) : Node(buildContext, plugins = plugins) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt index 6ef9d61a7b..85812b64a3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.voiceplayer.api.aVoiceMessageState fun aFakeTimelineItemPresenterFactories() = TimelineItemPresenterFactories( mapOf( Pair( - TimelineItemVoiceContent::class.java, + TimelineItemVoiceContent::class, TimelineItemPresenterFactory { Presenter { aVoiceMessageState() } }, ), ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/LiveTimeline.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/LiveTimeline.kt index 40624c9911..6fe88272af 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/LiveTimeline.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/LiveTimeline.kt @@ -7,7 +7,7 @@ package io.element.android.features.messages.impl.timeline.di -import javax.inject.Qualifier +import dev.zacsweers.metro.Qualifier @Retention(AnnotationRetention.RUNTIME) @MustBeDocumented diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemEventContentKey.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemEventContentKey.kt index bac308007b..e315da06f2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemEventContentKey.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemEventContentKey.kt @@ -7,13 +7,13 @@ package io.element.android.features.messages.impl.timeline.di -import dagger.MapKey +import dev.zacsweers.metro.MapKey import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import kotlin.reflect.KClass /** * Annotation to add a factory of type [TimelineItemPresenterFactory] to a - * Dagger map multi binding keyed with a subclass of [TimelineItemEventContent]. + * dependency injection map multi binding keyed with a subclass of [TimelineItemEventContent]. */ @Retention(AnnotationRetention.RUNTIME) @MapKey diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt index 96bf4de975..7df549223e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt @@ -9,25 +9,26 @@ package io.element.android.features.messages.impl.timeline.di import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.multibindings.Multibinds +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.Multibinds +import dev.zacsweers.metro.SingleIn import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.di.SingleIn -import javax.inject.Inject +import kotlin.reflect.KClass /** - * Dagger module that declares the [TimelineItemPresenterFactory] map multi binding. + * Container that declares the [TimelineItemPresenterFactory] map multi binding. * * Its sole purpose is to support the case of an empty map multibinding. */ -@Module +@BindingContainer @ContributesTo(RoomScope::class) interface TimelineItemPresenterFactoriesModule { @Multibinds - fun multiBindTimelineItemPresenterFactories(): @JvmSuppressWildcards Map, TimelineItemPresenterFactory<*, *>> + fun multiBindTimelineItemPresenterFactories(): @JvmSuppressWildcards Map, TimelineItemPresenterFactory<*, *>> } /** @@ -38,8 +39,9 @@ interface TimelineItemPresenterFactoriesModule { * goes out of the [LazyColumn] viewport. */ @SingleIn(RoomScope::class) -class TimelineItemPresenterFactories @Inject constructor( - private val factories: @JvmSuppressWildcards Map, TimelineItemPresenterFactory<*, *>>, +@Inject +class TimelineItemPresenterFactories( + private val factories: @JvmSuppressWildcards Map, TimelineItemPresenterFactory<*, *>>, ) { private val presenters: MutableMap> = mutableMapOf() @@ -57,7 +59,7 @@ class TimelineItemPresenterFactories @Inject constructor( @Composable fun rememberPresenter( content: C, - contentClass: Class, + contentClass: KClass, ): Presenter = remember(content) { presenters[content]?.let { @Suppress("UNCHECKED_CAST") @@ -86,5 +88,5 @@ inline fun TimelineItemPresenter content: C ): Presenter = rememberPresenter( content = content, - contentClass = C::class.java + contentClass = C::class ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactory.kt index 6954f7f37a..ccb5bec542 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactory.kt @@ -13,7 +13,7 @@ import io.element.android.libraries.architecture.Presenter /** * A factory for a [Presenter] associated with a timeline item. * - * Implementations should be annotated with [AssistedFactory] to be created by Dagger. + * Implementations should be annotated with [dev.zacsweers.metro.AssistedFactory] to be created by the dependency injection library. * * @param C The timeline item's [TimelineItemEventContent] subtype. * @param S The [Presenter]'s state class. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt index b998c0c815..a99bff0e50 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt @@ -7,9 +7,9 @@ package io.element.android.features.messages.impl.timeline.factories -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.messages.impl.timeline.diff.TimelineItemsCacheInvalidator import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory @@ -29,7 +29,8 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -class TimelineItemsFactory @AssistedInject constructor( +@AssistedInject +class TimelineItemsFactory( @Assisted config: TimelineItemsFactoryConfig, eventItemFactoryCreator: TimelineItemEventFactory.Creator, private val dispatchers: CoroutineDispatchers, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt index 2ffec3c1f4..4f1c9ba90a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt @@ -7,11 +7,16 @@ package io.element.android.features.messages.impl.timeline.factories.event -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent +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 @@ -19,6 +24,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInv import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent 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 @@ -26,9 +32,9 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StickerConten 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.getDisambiguatedDisplayName -import javax.inject.Inject -class TimelineItemContentFactory @Inject constructor( +@Inject +class TimelineItemContentFactory( private val messageFactory: TimelineItemContentMessageFactory, private val redactedMessageFactory: TimelineItemContentRedactedFactory, private val stickerFactory: TimelineItemContentStickerFactory, @@ -39,28 +45,55 @@ class TimelineItemContentFactory @Inject constructor( private val stateFactory: TimelineItemContentStateFactory, private val failedToParseMessageFactory: TimelineItemContentFailedToParseMessageFactory, private val failedToParseStateFactory: TimelineItemContentFailedToParseStateFactory, + private val sessionId: SessionId, ) { suspend fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent { - return when (val itemContent = eventTimelineItem.content) { + return create( + itemContent = eventTimelineItem.content, + eventId = eventTimelineItem.eventId, + isEditable = eventTimelineItem.isEditable, + sender = eventTimelineItem.sender, + senderProfile = eventTimelineItem.senderProfile, + ) + } + + suspend fun create( + itemContent: EventContent, + eventId: EventId?, + isEditable: Boolean, + sender: UserId, + senderProfile: ProfileTimelineDetails, + ): TimelineItemEventContent { + val isOutgoing = sessionId == sender + return when (itemContent) { is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent) is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent) is MessageContent -> { - val senderDisambiguatedDisplayName = eventTimelineItem.senderProfile.getDisambiguatedDisplayName(eventTimelineItem.sender) + val senderDisambiguatedDisplayName = senderProfile.getDisambiguatedDisplayName(sender) messageFactory.create( content = itemContent, senderDisambiguatedDisplayName = senderDisambiguatedDisplayName, - eventId = eventTimelineItem.eventId, + eventId = eventId, ) } - is ProfileChangeContent -> profileChangeFactory.create(eventTimelineItem) + is ProfileChangeContent -> { + val senderDisambiguatedDisplayName = senderProfile.getDisambiguatedDisplayName(sender) + profileChangeFactory.create(itemContent, isOutgoing, sender, senderDisambiguatedDisplayName) + } is RedactedContent -> redactedMessageFactory.create(itemContent) - is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem) + is RoomMembershipContent -> { + val senderDisambiguatedDisplayName = senderProfile.getDisambiguatedDisplayName(sender) + roomMembershipFactory.create(itemContent, isOutgoing, sender, senderDisambiguatedDisplayName) + } is LegacyCallInviteContent -> TimelineItemLegacyCallInviteContent - is StateContent -> stateFactory.create(eventTimelineItem) + is StateContent -> { + val senderDisambiguatedDisplayName = senderProfile.getDisambiguatedDisplayName(sender) + stateFactory.create(itemContent, isOutgoing, sender, senderDisambiguatedDisplayName) + } is StickerContent -> stickerFactory.create(itemContent) - is PollContent -> pollFactory.create(eventTimelineItem, itemContent) + is PollContent -> pollFactory.create(eventId, isEditable, isOutgoing, itemContent) is UnableToDecryptContent -> utdFactory.create(itemContent) - is CallNotifyContent -> TimelineItemCallNotifyContent() + is CallNotifyContent -> TimelineItemRtcNotificationContent() is UnknownContent -> TimelineItemUnknownContent } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFailedToParseMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFailedToParseMessageFactory.kt index d9608129d4..ae7e49c80d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFailedToParseMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFailedToParseMessageFactory.kt @@ -7,12 +7,13 @@ package io.element.android.features.messages.impl.timeline.factories.event +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent -import javax.inject.Inject -class TimelineItemContentFailedToParseMessageFactory @Inject constructor() { +@Inject +class TimelineItemContentFailedToParseMessageFactory { fun create(@Suppress("UNUSED_PARAMETER") failedToParseMessageLike: FailedToParseMessageLikeContent): TimelineItemEventContent { return TimelineItemUnknownContent } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFailedToParseStateFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFailedToParseStateFactory.kt index 6b805f59b4..38edb21b55 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFailedToParseStateFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFailedToParseStateFactory.kt @@ -7,12 +7,13 @@ package io.element.android.features.messages.impl.timeline.factories.event +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent -import javax.inject.Inject -class TimelineItemContentFailedToParseStateFactory @Inject constructor() { +@Inject +class TimelineItemContentFailedToParseStateFactory { @Suppress("UNUSED_PARAMETER") fun create(failedToParseState: FailedToParseStateContent): TimelineItemEventContent { return TimelineItemUnknownContent diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index f9e705b1ca..53dae43dfd 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 @@ -11,6 +11,7 @@ import android.text.style.URLSpan import androidx.core.text.buildSpannedString import androidx.core.text.getSpans import androidx.core.text.toSpannable +import dev.zacsweers.metro.Inject 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 @@ -48,10 +49,10 @@ import io.element.android.libraries.matrix.ui.messages.toHtmlDocument import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import javax.inject.Inject import kotlin.time.Duration -class TimelineItemContentMessageFactory @Inject constructor( +@Inject +class TimelineItemContentMessageFactory( private val fileSizeFormatter: FileSizeFormatter, private val fileExtensionExtractor: FileExtensionExtractor, private val htmlConverterProvider: HtmlConverterProvider, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt index c0da622d15..ec2eba6544 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt @@ -7,25 +7,28 @@ package io.element.android.features.messages.impl.timeline.factories.event +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.poll.api.pollcontent.PollContentStateFactory -import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.item.event.PollContent -import javax.inject.Inject -class TimelineItemContentPollFactory @Inject constructor( +@Inject +class TimelineItemContentPollFactory( private val pollContentStateFactory: PollContentStateFactory, ) { suspend fun create( - event: EventTimelineItem, + eventId: EventId?, + isEditable: Boolean, + isOwn: Boolean, content: PollContent, ): TimelineItemEventContent { - val pollContentState = pollContentStateFactory.create(event, content) + val pollContentState = pollContentStateFactory.create(eventId, isEditable, isOwn, content) return TimelineItemPollContent( isMine = pollContentState.isMine, isEditable = pollContentState.isPollEditable, - eventId = event.eventId, + eventId = eventId, question = pollContentState.question, answerItems = pollContentState.answerItems, pollKind = pollContentState.pollKind, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentProfileChangeFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentProfileChangeFactory.kt index e4d0809b85..a0a0b1f357 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentProfileChangeFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentProfileChangeFactory.kt @@ -7,18 +7,20 @@ package io.element.android.features.messages.impl.timeline.factories.event +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent import io.element.android.libraries.core.extensions.orEmpty import io.element.android.libraries.eventformatter.api.TimelineEventFormatter -import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem -import javax.inject.Inject +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.timeline.item.event.EventContent -class TimelineItemContentProfileChangeFactory @Inject constructor( +@Inject +class TimelineItemContentProfileChangeFactory( private val timelineEventFormatter: TimelineEventFormatter, ) { - fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent { - val text = timelineEventFormatter.format(eventTimelineItem) + fun create(content: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): TimelineItemEventContent { + val text = timelineEventFormatter.format(content, isOutgoing, sender, senderDisambiguatedDisplayName) return TimelineItemProfileChangeContent(text.orEmpty().toString()) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentRedactedFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentRedactedFactory.kt index 94b7ae3344..c79f2abbbc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentRedactedFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentRedactedFactory.kt @@ -7,12 +7,13 @@ package io.element.android.features.messages.impl.timeline.factories.event +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent -import javax.inject.Inject -class TimelineItemContentRedactedFactory @Inject constructor() { +@Inject +class TimelineItemContentRedactedFactory { fun create(@Suppress("UNUSED_PARAMETER") content: RedactedContent): TimelineItemEventContent { return TimelineItemRedactedContent } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentRoomMembershipFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentRoomMembershipFactory.kt index b1df3a2e10..a602e5274b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentRoomMembershipFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentRoomMembershipFactory.kt @@ -7,18 +7,20 @@ package io.element.android.features.messages.impl.timeline.factories.event +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent import io.element.android.libraries.core.extensions.orEmpty import io.element.android.libraries.eventformatter.api.TimelineEventFormatter -import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem -import javax.inject.Inject +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.timeline.item.event.EventContent -class TimelineItemContentRoomMembershipFactory @Inject constructor( +@Inject +class TimelineItemContentRoomMembershipFactory( private val timelineEventFormatter: TimelineEventFormatter, ) { - fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent { - val text = timelineEventFormatter.format(eventTimelineItem) + fun create(eventContent: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): TimelineItemEventContent { + val text = timelineEventFormatter.format(eventContent, isOutgoing, sender, senderDisambiguatedDisplayName) return TimelineItemRoomMembershipContent(text.orEmpty().toString()) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentStateFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentStateFactory.kt index 1b8ae0a560..6716a7ff83 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentStateFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentStateFactory.kt @@ -7,18 +7,20 @@ package io.element.android.features.messages.impl.timeline.factories.event +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent import io.element.android.libraries.core.extensions.orEmpty import io.element.android.libraries.eventformatter.api.TimelineEventFormatter -import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem -import javax.inject.Inject +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.timeline.item.event.EventContent -class TimelineItemContentStateFactory @Inject constructor( +@Inject +class TimelineItemContentStateFactory( private val timelineEventFormatter: TimelineEventFormatter, ) { - fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent { - val text = timelineEventFormatter.format(eventTimelineItem) + fun create(eventContent: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): TimelineItemEventContent { + val text = timelineEventFormatter.format(eventContent, isOutgoing, sender, senderDisambiguatedDisplayName) return TimelineItemStateEventContent(text.orEmpty().toString()) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentStickerFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentStickerFactory.kt index 94e75c48ef..0652f41365 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentStickerFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentStickerFactory.kt @@ -7,15 +7,16 @@ package io.element.android.features.messages.impl.timeline.factories.event +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor -import javax.inject.Inject -class TimelineItemContentStickerFactory @Inject constructor( +@Inject +class TimelineItemContentStickerFactory( private val fileSizeFormatter: FileSizeFormatter, private val fileExtensionExtractor: FileExtensionExtractor ) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentUTDFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentUTDFactory.kt index 120dbe73da..0d44b7bf65 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentUTDFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentUTDFactory.kt @@ -7,12 +7,13 @@ package io.element.android.features.messages.impl.timeline.factories.event +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent -import javax.inject.Inject -class TimelineItemContentUTDFactory @Inject constructor() { +@Inject +class TimelineItemContentUTDFactory { fun create(content: UnableToDecryptContent): TimelineItemEventContent { return TimelineItemEncryptedContent(content.data) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index eda3f2a3f6..6043cb57ff 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -7,9 +7,9 @@ package io.element.android.features.messages.impl.timeline.factories.event -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig import io.element.android.features.messages.impl.timeline.groups.canBeDisplayedInBubbleBlock import io.element.android.features.messages.impl.timeline.model.AggregatedReaction @@ -19,6 +19,9 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts +import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo +import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter +import io.element.android.libraries.architecture.map import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode @@ -36,12 +39,14 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import java.util.Date -class TimelineItemEventFactory @AssistedInject constructor( +@AssistedInject +class TimelineItemEventFactory( @Assisted private val config: TimelineItemsFactoryConfig, private val contentFactory: TimelineItemContentFactory, private val matrixClient: MatrixClient, private val dateFormatter: DateFormatter, private val permalinkParser: PermalinkParser, + private val summaryFormatter: MessageSummaryFormatter, ) { @AssistedFactory interface Creator { @@ -68,6 +73,29 @@ class TimelineItemEventFactory @AssistedInject constructor( url = senderProfile.getAvatarUrl(), size = AvatarSize.TimelineSender ) + val mappedThreadInfo = when (val threadInfo = currentTimelineItem.event.threadInfo()) { + is EventThreadInfo.ThreadResponse -> { + TimelineItemThreadInfo.ThreadResponse(threadInfo.threadRootId) + } + is EventThreadInfo.ThreadRoot -> { + TimelineItemThreadInfo.ThreadRoot( + summary = threadInfo.summary, + latestEventText = threadInfo.summary.latestEvent.dataOrNull() + ?.let { + contentFactory.create( + itemContent = it.content, + eventId = it.eventOrTransactionId.eventId, + isEditable = false, + sender = it.senderId, + senderProfile = it.senderProfile, + ) + } + ?.let(summaryFormatter::format) + ) + } + null -> null + } + return TimelineItem.Event( id = currentTimelineItem.uniqueId, eventId = currentTimelineItem.eventId, @@ -86,7 +114,7 @@ class TimelineItemEventFactory @AssistedInject constructor( readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers), localSendState = currentTimelineItem.event.localSendState, inReplyTo = currentTimelineItem.event.inReplyTo()?.map(permalinkParser = permalinkParser), - threadInfo = currentTimelineItem.event.threadInfo() ?: EventThreadInfo(threadRootId = null, threadSummary = null), + threadInfo = mappedThreadInfo, origin = currentTimelineItem.event.origin, timelineItemDebugInfoProvider = currentTimelineItem.event.timelineItemDebugInfoProvider, messageShieldProvider = currentTimelineItem.event.messageShieldProvider, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt index 066f495ab9..a93a8888ea 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt @@ -7,14 +7,15 @@ package io.element.android.features.messages.impl.timeline.factories.virtual +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem -import javax.inject.Inject -class TimelineItemDaySeparatorFactory @Inject constructor( +@Inject +class TimelineItemDaySeparatorFactory( private val dateFormatter: DateFormatter, ) { fun create(virtualItem: VirtualTimelineItem.DayDivider): TimelineItemVirtualModel { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt index 79af92bf93..bcb81ff9e4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt @@ -7,6 +7,7 @@ package io.element.android.features.messages.impl.timeline.factories.virtual +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLastForwardIndicatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel @@ -16,9 +17,9 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem -import javax.inject.Inject -class TimelineItemVirtualFactory @Inject constructor( +@Inject +class TimelineItemVirtualFactory( private val daySeparatorFactory: TimelineItemDaySeparatorFactory, ) { fun create( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt index 67a4d3da0d..a56a4d09e9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt @@ -9,7 +9,6 @@ package io.element.android.features.messages.impl.timeline.groups import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent @@ -19,6 +18,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent @@ -60,7 +60,7 @@ internal fun TimelineItem.Event.canBeGrouped(): Boolean { TimelineItemRedactedContent, TimelineItemUnknownContent, is TimelineItemLegacyCallInviteContent, - is TimelineItemCallNotifyContent -> false + is TimelineItemRtcNotificationContent -> false is TimelineItemProfileChangeContent, is TimelineItemRoomMembershipContent, is TimelineItemStateEventContent -> true diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt index 46ed097cd7..2bbcc4f3ba 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt @@ -8,15 +8,16 @@ package io.element.android.features.messages.impl.timeline.groups import androidx.annotation.VisibleForTesting +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.core.UniqueId import kotlinx.collections.immutable.toImmutableList -import javax.inject.Inject @SingleIn(RoomScope::class) -class TimelineItemGrouper @Inject constructor() { +@Inject +class TimelineItemGrouper { /** * Keys are identifier of items in a group, only one by group will be kept. * Values are the actual groupIds. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index 94e04dd1d8..5591616517 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -17,10 +17,11 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.SendHandle +import io.element.android.libraries.matrix.api.core.ThreadId 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.timeline.item.EventThreadInfo +import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary 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.LocalEventSendState @@ -82,7 +83,7 @@ sealed interface TimelineItem { val readReceiptState: TimelineItemReadReceipts, val localSendState: LocalEventSendState?, val inReplyTo: InReplyToDetails?, - val threadInfo: EventThreadInfo, + val threadInfo: TimelineItemThreadInfo?, val origin: TimelineItemEventOrigin?, val timelineItemDebugInfoProvider: TimelineItemDebugInfoProvider, val messageShieldProvider: MessageShieldProvider, @@ -130,3 +131,8 @@ sealed interface TimelineItem { val aggregatedReadReceipts: ImmutableList, ) : TimelineItem } + +sealed interface TimelineItemThreadInfo { + data class ThreadRoot(val summary: ThreadSummary, val latestEventText: String?) : TimelineItemThreadInfo + data class ThreadResponse(val threadRootId: ThreadId) : TimelineItemThreadInfo +} 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 d011865964..be7c47439a 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 @@ -81,7 +81,7 @@ fun TimelineItemEventContent.canReact(): Boolean = is TimelineItemStateContent, is TimelineItemRedactedContent, is TimelineItemLegacyCallInviteContent, - is TimelineItemCallNotifyContent, + is TimelineItemRtcNotificationContent, TimelineItemUnknownContent -> false } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemCallNotifyContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRtcNotificationContent.kt similarity index 65% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemCallNotifyContent.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRtcNotificationContent.kt index 75d070d025..0c2f21fc5b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemCallNotifyContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRtcNotificationContent.kt @@ -7,6 +7,6 @@ package io.element.android.features.messages.impl.timeline.model.event -class TimelineItemCallNotifyContent : TimelineItemEventContent { - override val type: String = "m.call.notify" +class TimelineItemRtcNotificationContent : TimelineItemEventContent { + override val type: String = "org.matrix.msc4075.rtc.notification" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt index 279a542081..1f306c74ea 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt @@ -9,7 +9,6 @@ package io.element.android.features.messages.impl.timeline.protection import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent @@ -21,6 +20,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent @@ -38,7 +38,7 @@ fun TimelineItem.mustBeProtected(): Boolean { is TimelineItemVideoContent, is TimelineItemStickerContent -> true is TimelineItemAudioContent, - is TimelineItemCallNotifyContent, + is TimelineItemRtcNotificationContent, is TimelineItemEncryptedContent, is TimelineItemFileContent, TimelineItemLegacyCallInviteContent, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt index b4c2576a65..0d9db51d98 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.matrix.api.core.EventId @@ -20,9 +21,9 @@ import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.media.isPreviewEnabled import io.element.android.libraries.matrix.api.room.BaseRoom import kotlinx.collections.immutable.toImmutableSet -import javax.inject.Inject -class TimelineProtectionPresenter @Inject constructor( +@Inject +class TimelineProtectionPresenter( private val mediaPreviewService: MediaPreviewService, private val room: BaseRoom, ) : Presenter { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt new file mode 100644 index 0000000000..62c9c3e206 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt @@ -0,0 +1,211 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.topbars + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +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.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.messages.impl.timeline.components.CallMenuItem +import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.features.roomcall.api.aStandByCallState +import io.element.android.features.roomcall.api.anOngoingCallState +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.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text +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.ui.components.aMatrixUserList +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun MessagesViewTopBar( + roomName: String?, + roomAvatar: AvatarData, + isTombstoned: Boolean, + heroes: ImmutableList, + roomCallState: RoomCallState, + dmUserIdentityState: IdentityState?, + onRoomDetailsClick: () -> Unit, + onJoinCallClick: () -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + navigationIcon = { + BackButton(onClick = onBackClick) + }, + title = { + val roundedCornerShape = RoundedCornerShape(8.dp) + Row( + modifier = Modifier + .clip(roundedCornerShape) + .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 + ) + + 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 = { + CallMenuItem( + roomCallState = roomCallState, + onJoinCallClick = onJoinCallClick, + ) + Spacer(Modifier.width(8.dp)) + }, + windowInsets = WindowInsets(0.dp) + ) +} + +@Composable +private fun RoomAvatarAndNameRow( + roomName: String?, + roomAvatar: AvatarData, + heroes: ImmutableList, + isTombstoned: Boolean, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Avatar( + avatarData = roomAvatar, + avatarType = AvatarType.Room( + heroes = heroes, + isTombstoned = isTombstoned, + ), + ) + Text( + 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 + ) + } +} + +@PreviewsDayNight +@Composable +internal fun MessagesViewTopBarPreview() = ElementPreview { + @Composable + fun AMessagesViewTopBar( + roomName: String? = "Room name", + roomAvatar: AvatarData = anAvatarData( + name = "Room name", + size = AvatarSize.TimelineRoom, + ), + isTombstoned: Boolean = false, + heroes: ImmutableList = persistentListOf(), + roomCallState: RoomCallState = RoomCallState.Unavailable, + dmUserIdentityState: IdentityState? = null, + ) = MessagesViewTopBar( + roomName = roomName, + roomAvatar = roomAvatar, + isTombstoned = isTombstoned, + heroes = heroes, + roomCallState = roomCallState, + dmUserIdentityState = dmUserIdentityState, + onRoomDetailsClick = {}, + onJoinCallClick = {}, + onBackClick = {}, + ) + Column { + AMessagesViewTopBar() + HorizontalDivider() + AMessagesViewTopBar( + heroes = aMatrixUserList().map { it.getAvatarData(AvatarSize.TimelineRoom) }.toImmutableList(), + roomCallState = anOngoingCallState(), + ) + HorizontalDivider() + AMessagesViewTopBar( + roomName = null, + roomCallState = anOngoingCallState(canJoinCall = false), + ) + HorizontalDivider() + AMessagesViewTopBar( + roomName = "A DM with a very very very long name", + roomAvatar = anAvatarData( + size = AvatarSize.TimelineRoom, + url = "https://some-avatar.jpg" + ), + roomCallState = aStandByCallState(canStartCall = false), + dmUserIdentityState = IdentityState.Verified + ) + HorizontalDivider() + AMessagesViewTopBar( + roomName = "A DM with a very very very long name", + isTombstoned = true, + dmUserIdentityState = IdentityState.VerificationViolation + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/ThreadTopBar.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/ThreadTopBar.kt new file mode 100644 index 0000000000..5a95c2c4b2 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/ThreadTopBar.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.features.messages.impl.topbars + +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.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.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +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.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun ThreadTopBar( + roomName: String?, + roomAvatarData: AvatarData, + heroes: ImmutableList, + isTombstoned: Boolean, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + navigationIcon = { + BackButton(onClick = onBackClick) + }, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Avatar( + avatarData = roomAvatarData, + avatarType = AvatarType.Room( + heroes = heroes, + isTombstoned = isTombstoned, + ), + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .semantics { + heading() + }, + ) { + Text( + text = stringResource(CommonStrings.common_thread), + style = ElementTheme.typography.fontBodyLgMedium, + ) + Text( + text = roomName ?: stringResource(CommonStrings.common_no_room_name), + style = ElementTheme.typography.fontBodySmRegular, + fontStyle = FontStyle.Italic.takeIf { roomName == null }, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + ) +} + +@PreviewsDayNight +@Composable +internal fun ThreadTopBarPreview() = ElementPreview { + @Composable + fun AThreadTopBar( + roomName: String? = "Room name", + roomAvatarData: AvatarData = anAvatarData( + name = "Room name", + size = AvatarSize.TimelineRoom, + ), + isTombstoned: Boolean = false, + heroes: ImmutableList = persistentListOf(), + ) = ThreadTopBar( + roomName = roomName, + roomAvatarData = roomAvatarData, + isTombstoned = isTombstoned, + heroes = heroes, + onBackClick = {}, + ) + Column { + AThreadTopBar() + HorizontalDivider() + AThreadTopBar( + heroes = aMatrixUserList().map { it.getAvatarData(AvatarSize.TimelineRoom) }.toImmutableList(), + ) + HorizontalDivider() + AThreadTopBar( + roomName = null, + ) + HorizontalDivider() + AThreadTopBar( + roomAvatarData = anAvatarData( + name = "Room name", + url = "https://some-avatar.jpg", + size = AvatarSize.TimelineRoom, + ), + ) + HorizontalDivider() + AThreadTopBar( + isTombstoned = true, + ) + } +} 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 7d293055a2..bedbf8104c 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 @@ -16,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.JoinedRoom @@ -29,9 +30,9 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import javax.inject.Inject -class TypingNotificationPresenter @Inject constructor( +@Inject +class TypingNotificationPresenter( private val room: JoinedRoom, private val sessionPreferencesStore: SessionPreferencesStore, ) : Presenter { 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 458c27061c..bb95e1a26c 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 @@ -13,7 +13,8 @@ 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 dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.MatrixPatternType import io.element.android.libraries.matrix.api.core.MatrixPatterns @@ -26,14 +27,14 @@ 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, pillifyPermalinks: Boolean = true): CharSequence } @ContributesBinding(RoomScope::class) -class DefaultTextPillificationHelper @Inject constructor( +@Inject +class DefaultTextPillificationHelper( private val mentionSpanProvider: MentionSpanProvider, private val permalinkParser: PermalinkParser, private val permalinkBuilder: PermalinkBuilder, 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 8a05770942..1f1619e3f6 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 @@ -8,11 +8,11 @@ package io.element.android.features.messages.impl.utils.messagesummary import android.content.Context -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.messages.impl.timeline.model.TimelineItem +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent 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.TimelineItemLegacyCallInviteContent @@ -20,6 +20,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent @@ -27,24 +28,24 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.libraries.core.extensions.toSafeLength -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.ui.strings.CommonStrings -import javax.inject.Inject @ContributesBinding(RoomScope::class) -class DefaultMessageSummaryFormatter @Inject constructor( +@Inject +class DefaultMessageSummaryFormatter( @ApplicationContext private val context: Context, ) : MessageSummaryFormatter { - override fun format(event: TimelineItem.Event): String { - return when (event.content) { - is TimelineItemTextBasedContent -> event.content.plainText - is TimelineItemProfileChangeContent -> event.content.body - is TimelineItemStateContent -> event.content.body + override fun format(content: TimelineItemEventContent): String { + return when (content) { + is TimelineItemTextBasedContent -> content.plainText + is TimelineItemProfileChangeContent -> content.body + is TimelineItemStateContent -> content.body is TimelineItemLocationContent -> context.getString(CommonStrings.common_shared_location) is TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt) is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed) - is TimelineItemPollContent -> event.content.question + is TimelineItemPollContent -> content.question is TimelineItemVoiceContent -> context.getString(CommonStrings.common_voice_message) is TimelineItemUnknownContent -> context.getString(CommonStrings.common_unsupported_event) is TimelineItemImageContent -> context.getString(CommonStrings.common_image) @@ -53,7 +54,7 @@ class DefaultMessageSummaryFormatter @Inject constructor( is TimelineItemFileContent -> context.getString(CommonStrings.common_file) is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio) is TimelineItemLegacyCallInviteContent -> context.getString(CommonStrings.common_unsupported_call) - is TimelineItemCallNotifyContent -> context.getString(CommonStrings.common_call_started) + is TimelineItemRtcNotificationContent -> context.getString(CommonStrings.common_call_started) } // Truncate the message to a safe length to avoid crashes in Compose .toSafeLength() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatter.kt index 393ed21fa5..6e590c8779 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatter.kt @@ -8,7 +8,11 @@ package io.element.android.features.messages.impl.utils.messagesummary import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent interface MessageSummaryFormatter { - fun format(event: TimelineItem.Event): String + fun format(event: TimelineItem.Event): String { + return format(event.content) + } + fun format(content: TimelineItemEventContent): String } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt index b8ecab747a..8e06a3a1d2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt @@ -19,10 +19,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.core.net.toUri import androidx.lifecycle.Lifecycle -import com.squareup.anvil.annotations.ContributesBinding -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesBinding import im.vector.app.features.analytics.plan.Composer import io.element.android.features.messages.api.MessageComposerContext import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents @@ -51,7 +51,8 @@ import java.io.File import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds -class DefaultVoiceMessageComposerPresenter @AssistedInject constructor( +@AssistedInject +class DefaultVoiceMessageComposerPresenter( @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, @Assisted private val timelineMode: Timeline.Mode, private val voiceRecorder: VoiceRecorder, 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 57b18817f5..a8ca37dac7 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 @@ -7,6 +7,7 @@ package io.element.android.features.messages.impl.voicemessages.composer +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.mediaplayer.api.MediaPlayer @@ -21,7 +22,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.scan import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject /** * A media player for the voice message composer. @@ -29,7 +29,8 @@ import javax.inject.Inject * @param mediaPlayer The [MediaPlayer] to use. * @param sessionCoroutineScope */ -class VoiceMessageComposerPlayer @Inject constructor( +@Inject +class VoiceMessageComposerPlayer( private val mediaPlayer: MediaPlayer, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManager.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManager.kt index 087ed26d06..be1db85d6b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManager.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManager.kt @@ -7,21 +7,22 @@ package io.element.android.features.messages.impl.voicemessages.timeline -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent import io.element.android.libraries.mediaplayer.api.MediaPlayer import kotlinx.coroutines.withContext -import javax.inject.Inject interface RedactedVoiceMessageManager { suspend fun onEachMatrixTimelineItem(timelineItems: List) } @ContributesBinding(RoomScope::class) -class DefaultRedactedVoiceMessageManager @Inject constructor( +@Inject +class DefaultRedactedVoiceMessageManager( private val dispatchers: CoroutineDispatchers, private val mediaPlayer: MediaPlayer, ) : RedactedVoiceMessageManager { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt index 99b63c6c3d..2931f6af53 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt @@ -8,13 +8,13 @@ package io.element.android.features.messages.impl.voicemessages.timeline import androidx.compose.runtime.Composable -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.multibindings.IntoMap +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.IntoMap import io.element.android.features.messages.impl.timeline.di.TimelineItemEventContentKey import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactory import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent @@ -23,7 +23,7 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.voiceplayer.api.VoiceMessagePresenterFactory import io.element.android.libraries.voiceplayer.api.VoiceMessageState -@Module +@BindingContainer @ContributesTo(RoomScope::class) interface VoiceMessagePresenterModule { @Binds @@ -32,7 +32,8 @@ interface VoiceMessagePresenterModule { fun bindVoiceMessagePresenterFactory(factory: VoiceMessagePresenter.Factory): TimelineItemPresenterFactory<*, *> } -class VoiceMessagePresenter @AssistedInject constructor( +@AssistedInject +class VoiceMessagePresenter( voiceMessagePresenterFactory: VoiceMessagePresenterFactory, @Assisted private val content: TimelineItemVoiceContent, ) : Presenter { 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 bd910f2e58..8efecfaf26 100644 --- a/features/messages/impl/src/main/res/values-bg/translations.xml +++ b/features/messages/impl/src/main/res/values-bg/translations.xml @@ -8,7 +8,15 @@ "Усмивки & Хора" "Пътуване & Места" "Символи" + "Докоснете, за да промените качеството на качване на видео" + "Файлът не можа да бъде качен." + "Неуспешна обработка на мултимедия за качване, моля, опитайте отново." + "Неуспешно качване на мултимедия, моля, опитайте отново." + "Файлът е твърде голям за качване" "Блокиране на потребителя" + "Отметнете ако искате да скриете всички настоящи и бъдещи съобщения от този потребител" + "Това съобщение ще бъде докладвано на администратора на вашия сървър. Те няма да могат да четат шифровани съобщения." + "Причина за докладване на това съдържание" "Камера" "Снимка" "Запис на видео" @@ -18,12 +26,17 @@ "Анкета" "Форматиране на текст" "Хронологията на съобщенията не е налична в момента." + "Искате ли да ги поканите обратно?" + "Вие сте сами в този чат" "Всеки" + "Изпращане отново" + "Вашето съобщение не успя да се изпрати" "Добавяне на емоджи" "Това е началото на %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 c3707a2379..3b9ffb42a3 100644 --- a/features/messages/impl/src/main/res/values-cs/translations.xml +++ b/features/messages/impl/src/main/res/values-cs/translations.xml @@ -7,13 +7,18 @@ "Předměty" "Smajlíci a lidé" "Cestování a místa" + "Nedávné emotikony" "Symboly" "Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace." + "Klepnutím změníte kvalitu nahrávání videa" "Soubor nelze nahrát." "Nahrání média se nezdařilo, zkuste to prosím znovu." "Nahrání média se nezdařilo, zkuste to prosím znovu." "Maximální povolená velikost souboru je %1$s." "Soubor je pro nahrání příliš velký." + "Položka %1$d z %2$d" + "Optimalizace kvality obrazu" + "Probíhá zpracování…" "Zablokovat uživatele" "Zaškrtněte, pokud chcete skrýt všechny aktuální a budoucí zprávy od tohoto uživatele" "Tato zpráva bude nahlášena správci vašeho domovského serveru. Nebude si moci přečíst žádné šifrované zprávy." diff --git a/features/messages/impl/src/main/res/values-cy/translations.xml b/features/messages/impl/src/main/res/values-cy/translations.xml index 16841b55f6..5591e3abd7 100644 --- a/features/messages/impl/src/main/res/values-cy/translations.xml +++ b/features/messages/impl/src/main/res/values-cy/translations.xml @@ -7,10 +7,18 @@ "Gwrthrychau" "Wynebau Hapus a Phobl" "Teithio a Llefydd" + "Emojis diweddar" "Symbolau" "Efallai na fydd capsiynau yn weladwy i bobl sy\'n defnyddio apiau hŷn." + "Tapiwch i newid ansawdd llwytho\'r fideo" + "Nid oedd modd llwytho\'r ffeil." "Wedi methu â phrosesu cyfryngau i\'w llwytho, ceisiwch eto." "Wedi methu llwytho cyfryngau, ceisiwch eto." + "Y maint ffeil mwyaf a ganiateir yw %1$s ." + "Mae\'r ffeil yn rhy fawr i\'w llwytho" + "Eitem %1$d o %2$d" + "Optimeiddio ansawdd delwedd" + "Prosesu…" "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." @@ -38,6 +46,14 @@ "Dangos llai" "Neges wedi\'i chopïo" "Does gennych chi ddim caniatâd i bostio i\'r ystafell hon" + + "Ymatebodd %1$d aelodau gyda %2$s" + "Ymatebodd %1$d aelodau gyda %2$s" + "Ymatebodd %1$d aelod gyda %2$s" + "Ymatebodd %1$d aelod gyda %2$s" + "Ymatebodd %1$d aelod gyda %2$s" + "Ymatebodd %1$d aelod gyda %2$s" + "Rydych chi wedi ymateb gyda %1$s" "Dangos llai" "Dangos rhagor" diff --git a/features/messages/impl/src/main/res/values-da/translations.xml b/features/messages/impl/src/main/res/values-da/translations.xml index 5aba4e77c5..af7b0f003e 100644 --- a/features/messages/impl/src/main/res/values-da/translations.xml +++ b/features/messages/impl/src/main/res/values-da/translations.xml @@ -5,8 +5,9 @@ "Mad og drikke" "Dyr og natur" "Objekter" - "Smileys og mennesker" + "Smileys og personer" "Rejser og steder" + "Seneste emojis" "Symboler" "Billedtekster er muligvis ikke synlige for personer, der bruger ældre apps." "Tryk for at ændre videokvaliteten i uploadet" @@ -15,6 +16,7 @@ "Upload af medier mislykkedes. Prøv igen." "Den maksimalt tilladte filstørrelse er %1$s ." "Filen er for stor til at kunne uploades." + "Fil %1$d af %2$d" "Optimér billedkvaliteten" "Behandler…" "Bloker bruger" 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 a2bac9fbf2..458be8a3b9 100644 --- a/features/messages/impl/src/main/res/values-de/translations.xml +++ b/features/messages/impl/src/main/res/values-de/translations.xml @@ -7,26 +7,34 @@ "Objekte" "Smileys & Menschen" "Reisen & Orte" + "Zuletzt verwendete Emojis" "Symbole" "Bildunterschriften sind für Nutzer älterer Apps möglicherweise nicht sichtbar." + "Tippe, um die Qualität des Video-Uploads zu ändern" + "Die Datei konnte nicht hochgeladen werden." "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." "Das Hochladen der Medien ist fehlgeschlagen. Bitte versuche es erneut." + "Die maximal zulässige Dateigröße beträgt %1$s." + "Die Datei ist zu groß zum Hochladen." + "%1$d von %2$d" + "Optimiere die Bildqualität" + "Verarbeitung läuft …" "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." + "Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Nutzers ausblenden möchtest" + "Diese Nachricht wird dem Administrator deines Homeservers gemeldet. Dieser kann allerdings keine verschlüsselten Nachrichten lesen." "Grund für die Meldung dieses Inhalts" "Kamera" "Foto aufnehmen" "Video aufnehmen" "Anhang" - "Foto- und Videobibliothek" + "Foto- und Videogalerie" "Standort" "Umfrage" "Textformatierung" "Der Nachrichtenverlauf ist derzeit nicht verfügbar" "Der Nachrichtenverlauf ist nicht verfügbar. Verifiziere dieses Gerät, um deinen Nachrichtenverlauf zu sehen." - "Möchten Sie sie erneut einladen?" - "Sie sind in diesem Chat allein" + "Möchtest du sie wieder einladen?" + "Du bist allein in diesem Chat" "Alle Mitglieder benachrichtigen" "Alle" "Erneut senden" @@ -34,10 +42,10 @@ "Emoji hinzufügen" "Dies ist der Anfang von %1$s." "Dies ist der Anfang dieses Gesprächs." - "Anruftyp wird nicht unterstützt. Fragen Sie nach, ob der Anrufer die neue Element X-App verwenden kann." + "Nicht unterstützter Anruf. Frag den Anrufer, ob er die neue Element X-App nutzen kann." "Weniger anzeigen" "Nachricht wurde kopiert" - "Du bist nicht berechtigt, in diesem Raum zu schreiben" + "Du bist nicht berechtigt, in diesem Chat zu schreiben" "%1$d Mitglied reagierte mit %2$s" "%1$d Mitglieder reagierten mit %2$s" @@ -52,13 +60,13 @@ "Zusammenfassung der Reaktionen anzeigen" "Neu" - "%1$d Raumänderung" - "%1$d Raumänderungen" + "%1$d Änderung im Chat" + "%1$d Änderungen im Chat" - "Zum neuen Raum springen" - "Dieser Raum wurde ersetzt und ist nicht mehr aktiv" + "Zum Nachfolge-Chat springen" + "Dieser Chat wurde stillgelegt und ist nicht mehr aktiv" "Alte Nachrichten ansehen" - "Dieser Raum ist eine Fortsetzung eines anderen Raums" + "Dieser Chat ist eine Fortsetzung eines anderen Chats" "%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-eo/translations.xml b/features/messages/impl/src/main/res/values-eo/translations.xml new file mode 100644 index 0000000000..a0810ca15b --- /dev/null +++ b/features/messages/impl/src/main/res/values-eo/translations.xml @@ -0,0 +1,4 @@ + + + "Message history is unavailable in this room. Confirm this device to see your message history." + 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 3e07759a68..b2129f4ffd 100644 --- a/features/messages/impl/src/main/res/values-et/translations.xml +++ b/features/messages/impl/src/main/res/values-et/translations.xml @@ -7,6 +7,7 @@ "Esemed" "Emotikonid ja inimesed" "Reisimine ja kohad" + "Hiljutised emojid" "Sümbolid" "Selgitused ja alapealkirjad ei pruugi olla nähtavad vanemate rakenduste kasutajatele." "Klõpsa üleslaaditava video kvaliteedi muutmiseks" @@ -15,6 +16,7 @@ "Meediafaili üleslaadimine ei õnnestunud. Palun proovi uuesti." "Maksimaalne lubatud failisuurus on %1$s." "Fail on üleslaadimiseks liiga suur" + "Objekt %1$d/%2$d" "Optimeeri pildikvaliteeti" "Töötlen…" "Blokeeri kasutaja" 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 0f4a273f0f..3e1af3ce01 100644 --- a/features/messages/impl/src/main/res/values-fi/translations.xml +++ b/features/messages/impl/src/main/res/values-fi/translations.xml @@ -7,13 +7,17 @@ "Esineet" "Hymiöt ja ihmiset" "Matkustaminen ja paikat" + "Viimeaikaiset emojit" "Symbolit" "Kuvatekstit eivät välttämättä näy ihmisille, jotka käyttävät vanhempia sovelluksia." + "Napauta muuttaaksesi videon lähetyslaatua" "Tiedostoa ei voitu lähettää." "Median käsittely epäonnistui, yritä uudelleen." "Median lähettäminen epäonnistui, yritä uudelleen." "Suurin sallittu tiedostokoko on %1$s." "Tiedosto on liian suuri lähetettäväksi" + "Optimoi kuvanlaatu" + "Käsitellään…" "Estä käyttäjä" "Valitse tämä, jos haluat piilottaa kaikki nykyiset ja tulevat viestit tältä käyttäjältä" "Tämä viesti ilmoitetaan kotipalvelimesi ylläpitäjälle. Ylläpitäjä ei pysty lukemaan salattuja viestejä." diff --git a/features/messages/impl/src/main/res/values-fr/translations.xml b/features/messages/impl/src/main/res/values-fr/translations.xml index 76b9ae22ee..cb8e65fd01 100644 --- a/features/messages/impl/src/main/res/values-fr/translations.xml +++ b/features/messages/impl/src/main/res/values-fr/translations.xml @@ -7,6 +7,7 @@ "Objets" "Émoticônes et personnes" "Voyages & lieux" + "Emojis récents" "Symboles" "Les légendes peuvent ne pas être visibles pour les utilisateurs d’anciennes applications." "Cliquez pour modifier la qualité d’envoi de la vidéo" @@ -15,6 +16,7 @@ "Échec du téléchargement du média, veuillez réessayer." "La taille maximale autorisée pour les fichiers est de %1$s." "Le fichier est trop volumineux pour être envoyé." + "Élément %1$d sur %2$d" "Optimiser la qualité de l’image" "Traitement en cours…" "Bloquer l’utilisateur" 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 d72afc0078..f859aa516a 100644 --- a/features/messages/impl/src/main/res/values-hu/translations.xml +++ b/features/messages/impl/src/main/res/values-hu/translations.xml @@ -7,6 +7,7 @@ "Tárgyak" "Mosolyok és emberek" "Utazás és helyek" + "Legutóbbi emodzsik" "Szimbólumok" "Előfordulhat, hogy a feliratok nem láthatók a régebbi alkalmazásokat használók számára." "Koppintson a feltöltött videók minőségének módosításához" @@ -15,6 +16,7 @@ "Nem sikerült a média feltöltése, próbálja újra." "A maximálisan megengedett fájlméret: %1$s ." "A fájl túl nagy a feltöltéshez" + "%1$d. elem / %2$d" "Képminőség optimalizációja" "Feldolgozás…" "Felhasználó letiltása" 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 f0d7e95766..87a7a05c93 100644 --- a/features/messages/impl/src/main/res/values-it/translations.xml +++ b/features/messages/impl/src/main/res/values-it/translations.xml @@ -9,8 +9,14 @@ "Viaggi & Luoghi" "Simboli" "Le didascalie potrebbero non essere visibili agli utenti di app meno recenti." + "Tocca per modificare la qualità di caricamento del video" + "Impossibile caricare il file." "Elaborazione del file multimediale da caricare fallita, riprova." "Caricamento del file multimediale fallito, riprova." + "La dimensione massima consentita del file è %1$s ." + "Il file è troppo grande per essere caricato" + "Ottimizza la qualità delle immagini" + "Elaborazione…" "Blocca utente" "Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente" "Questo messaggio verrà segnalato all\'amministratore dell\'homeserver. Questi non sarà in grado di leggere i messaggi cifrati." diff --git a/features/messages/impl/src/main/res/values-ko/translations.xml b/features/messages/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..ddcecb58e8 --- /dev/null +++ b/features/messages/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,72 @@ + + + "활동" + "깃발" + "음식 & 음료" + "동물 & 자연" + "사물" + "표정 & 사람" + "여행 & 장소" + "상징" + "캡션은 오래된 앱을 사용하는 사용자에게 표시되지 않을 수 있습니다." + "비디오 업로드 품질을 변경하려면 탭하세요" + "파일을 업로드할 수 없습니다." + "미디어 업로드 처리가 실패했습니다. 다시 시도해 주세요." + "미디어 파일 업로드에 실패했습니다. 다시 시도해 주세요." + "허용되는 최대 파일 크기는 %1$s 입니다." + "파일 크기가 너무 커서 업로드할 수 없습니다." + "이미지 품질 최적화" + "처리 중…" + "사용자 차단하기" + "이 사용자의 현재 및 향후 모든 메시지를 숨기려면 확인하세요." + "이 메시지는 홈서버의 관리자에게 보고되었습니다. 암호화된 메시지는 읽을 수 없습니다." + "이 콘텐츠를 신고하는 이유" + "카메라" + "사진 찍기" + "동영상 녹화" + "첨부 파일" + "사진 & 동영상 라이브러리" + "위치" + "투표" + "텍스트 서식" + "메시지 기록은 현재 사용할 수 없습니다." + "이 룸에서는 메시지 기록을 사용할 수 없습니다. 이 기기를 확인하여 메시지 기록을 확인하세요." + "그들을 다시 초대하시겠습니까?" + "이 채팅에는 귀하만 있습니다." + "방 전체에 알림" + "모두" + "다시 보내기" + "메시지 전송에 실패했습니다." + "반응 추가" + "%1$s의 시작입니다." + "대화의 시작입니다." + "지원되지 않는 통화입니다. 발신자에게 새로운 Element X 앱을 사용할 수 있는지 문의하시기 바랍니다." + "덜 보기" + "메시지 복사됨" + "이 방에 게시할 수 있는 권한이 없습니다." + + "%1$d 회원들이 반응했습니다: %2$s" + + + "당신과 %1$d 멤버들은 다음과 같이 반응했습니다 %2$s" + + "당신은 다음과 같이 반응했습니다 %1$s" + "덜 보기" + "더 보기" + "반응 요약 표시" + "신규" + + "%1$d 방 변경" + + "새로운 방으로 이동" + "이 방은 대체되어 더 이상 활성화되어 있지 않습니다" + "이전 메시지 보기" + "이 방은 다른 방의 연속입니다." + + "%1$s, %2$s 및 %3$d 기타" + + + "%1$s 입력 중입니다" + + "%1$s 그리고 %2$s" + diff --git a/features/messages/impl/src/main/res/values-nb/translations.xml b/features/messages/impl/src/main/res/values-nb/translations.xml index 18bf54b1fd..deee637c09 100644 --- a/features/messages/impl/src/main/res/values-nb/translations.xml +++ b/features/messages/impl/src/main/res/values-nb/translations.xml @@ -9,8 +9,11 @@ "Reising og steder" "Symboler" "Teksting er kanskje ikke synlig for personer som bruker eldre apper." + "Filen kunne ikke lastes opp." "Kunne ikke behandle medier for opplasting, vennligst prøv igjen." "Opplasting av medier mislyktes, vennligst prøv igjen." + "Maksimal tillatt filstørrelse er %1$s." + "Filen er for stor til å lastes opp" "Blokker bruker" "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." 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 190b50d2ad..c85f17bdb3 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 @@ -8,13 +8,16 @@ "Sorrisos & Pessoas" "Viagens & Lugares" "Símbolos" - "As legendas podem não ser visíveis para pessoas que usam aplicativos mais antigos." - "Falha ao processar mídia para upload. Tente novamente." + "As legendas podem não ser visíveis para pessoas que usam apps mais antigos." + "O arquivo não pôde ser enviado." + "Falha ao processar a mídia para o envio. Tente novamente." "Falha ao enviar mídia. Tente novamente." + "O tamanho de arquivo máximo permitido é %1$s." + "O arquivo é muito grande para enviar" "Bloquear usuário" "Marque se você deseja ocultar todas as mensagens atuais e futuras desse usuário" - "Essa mensagem será reportada ao administrador do seu homeserver. Eles não conseguirão ler nenhuma mensagem criptografada." - "Motivo para denunciar este conteúdo" + "Essa mensagem será reportada ao administrador do seu servidor-casa. Eles não conseguirão ler nenhuma mensagem criptografada." + "Motivo por denunciar este conteúdo" "Câmera" "Tirar foto" "Gravar vídeo" @@ -26,15 +29,15 @@ "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" + "Você está sozinho nesta conversa" "Notificar a sala inteira" "Todos" "Enviar novamente" "Sua mensagem não foi enviada" "Adicionar emoji" - "Este é o início do %1$s." + "Este é o início de %1$s." "Este é o início desta conversa." - "Chamada não suportada. Pergunte se o chamador pode usar o novo aplicativo Element X." + "Chamada não suportada. Pergunte se o remetente pode usar o novo aplicativo Element X." "Mostrar menos" "Mensagem copiada" "Você não tem permissão para postar nesta sala" 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 4ed5faff55..3848f04063 100644 --- a/features/messages/impl/src/main/res/values-pt/translations.xml +++ b/features/messages/impl/src/main/res/values-pt/translations.xml @@ -7,13 +7,18 @@ "Objetos" "Caras e Pessoas" "Viagens e Lugares" + "Emojis recentes" "Símbolos" "As legendas poderão não ser visíveis em versões mais antigas da aplicação." + "Toca para alterar a qualidade de carregamento do vídeo" "Não foi possível enviar o ficheiro" "Falha ao processar multimédia para carregamento, por favor tente novamente." "Falhar ao carregar multimédia, por favor tente novamente." "O tamanho máximo permitido é %1$s." "O ficheiro é demasiado grande para enviar" + "Item %1$d de %2$d" + "Optimiza a qualidade da imagem" + "A processar…" "Bloquear 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." diff --git a/features/messages/impl/src/main/res/values-ro/translations.xml b/features/messages/impl/src/main/res/values-ro/translations.xml index 3ca5002f38..da8344f18a 100644 --- a/features/messages/impl/src/main/res/values-ro/translations.xml +++ b/features/messages/impl/src/main/res/values-ro/translations.xml @@ -8,8 +8,15 @@ "Fețe zâmbitoare & Oameni" "Călătorii & Locuri" "Simboluri" + "Este posibil ca descrierile să nu fie vizibile pentru persoanele care folosesc aplicații mai vechi." + "Atingeți pentru a modifica calitatea încărcării videoclipului" + "Fișierul nu a putut fi încărcat." "Procesarea datelor media a eșuat, vă rugăm să încercați din nou." "Încărcarea fișierelor media a eșuat, încercați din nou." + "Dimensiunea maximă permisă pentru fișiere este de %1$s." + "Fișierul este prea mare pentru a fi încărcat." + "Optimizați calitatea imaginii" + "Se procesează…" "Blocați utilizatorul" "Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator" "Acest mesaj va fi raportat administratorilor homeserver-ului tau. Ei nu vor putea citi niciun mesaj criptat." @@ -22,8 +29,8 @@ "Locație" "Sondaj" "Formatarea textului" - "Istoricul mesajelor este momentan indisponibil în această cameră" - "Istoricul mesajelor nu este disponibil în această cameră. Verificați acest dispozitiv pentru a vedea istoricul mesajelor." + "Mesajele anterioare nu sunt momentan disponibile în această cameră" + "Mesajele anterioare nu sunt disponibile în această cameră. Verificați acest dispozitiv pentru a vedea mesajele anterioare." "Doriți să îi invitați înapoi?" "Sunteți singur în această cameră" "Notificați întreaga cameră" @@ -33,17 +40,34 @@ "Adăugați emoji" "Acesta este începutul conversației %1$s." "Acesta este începutul acestei conversații." + "Apel neacceptat. Întrebați apelantul dacă poate utiliza noua aplicație Element X." "Afișați mai puțin" "Mesaj copiat" "Nu aveți permisiunea de a posta în această cameră" + + "%1$d membru a reacționat cu %2$s" + "%1$d membri au reacționat cu %2$s" + "%1$d membri au reacționat cu %2$s" + + + "Dumneavoastră si %1$d membru ați reacționat cu %2$s" + "Dumneavoastră si %1$d membri ați reacționat cu %2$s" + "Dumneavoastră si %1$d membri ați reacționat cu %2$s" + + "Ați reacționat cu %1$s" "Afișați mai puțin" "Afișați mai mult" + "Afișați rezumatul reacțiilor" "Nou" "%1$d schimbare a camerii" "%1$d schimbări ale camerei" "%1$d schimbări ale camerei" + "Săriți la noua cameră" + "Această cameră a fost înlocuită și nu mai este activă." + "Vedeți mesajele vechi" + "Această cameră este o continuare a unei alte camere." "%1$s, %2$s și încă %3$d" "%1$s, %2$s și încă %3$d" 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 61d1efa0b8..0cc79fba16 100644 --- a/features/messages/impl/src/main/res/values-ru/translations.xml +++ b/features/messages/impl/src/main/res/values-ru/translations.xml @@ -38,8 +38,20 @@ "Показать меньше" "Сообщение скопировано" "У вас нет разрешения публиковать сообщения в этой комнате" + + "%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 изменение в комнате" 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 5ecb88af84..cd5423b64a 100644 --- a/features/messages/impl/src/main/res/values-sv/translations.xml +++ b/features/messages/impl/src/main/res/values-sv/translations.xml @@ -9,8 +9,14 @@ "Resor & platser" "Symboler" "Bildtexter kanske inte är synliga för personer som använder äldre appar." + "Tryck för att ändra videouppladdningskvaliteten" + "Filen kunde inte laddas upp." "Misslyckades att bearbeta media för uppladdning, vänligen pröva igen." "Misslyckades att ladda upp media, vänligen pröva igen." + "Den maximala tillåtna filstorleken är %1$s." + "Filen är för stor för att laddas upp" + "Optimera bildkvalitet" + "Bearbetar …" "Blockera användare" "Markera om du vill dölja alla nuvarande och framtida meddelanden från denna användare" "Det här meddelandet kommer att rapporteras till din hemservers administratör. Denne kommer inte att kunna läsa några krypterade meddelanden." diff --git a/features/messages/impl/src/main/res/values-uz/translations.xml b/features/messages/impl/src/main/res/values-uz/translations.xml index 5e740ee6c1..e48de2b6ad 100644 --- a/features/messages/impl/src/main/res/values-uz/translations.xml +++ b/features/messages/impl/src/main/res/values-uz/translations.xml @@ -26,6 +26,7 @@ "Xabar tarixi ushbu xonada mavjud emas. Xabar tarixini koʻrish uchun ushbu qurilmani tasdiqlang." "Ularni yana taklif qilmoqchimisiz?" "Siz bu chatda yolg\'izsiz" + "Butun xonani xabardor qiling" "Har kim" "Yana yuboring" "Xabaringiz yuborilmadi" @@ -42,4 +43,13 @@ "%1$dxonani almashtirish" "%1$dxona o\'zgarishi" + + "%1$s, %2$s va %3$d boshqalar" + "%1$s, %2$s va %3$d boshqalar" + + + "%s yozmoqda…" + "%s yozmoqda…" + + "%1$s va %2$s" diff --git a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml index 7854d5a0f3..6abf951e4f 100644 --- a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml @@ -9,11 +9,14 @@ "旅行與景點" "標誌" "使用舊應用程式的使用者可能看不到標題。" + "輕點即可變更影片上傳品質" "無法上傳檔案。" "無法處理要上傳的媒體,請再試一次。" "無法上傳媒體檔案,請稍後再試。" "允許的最大檔案大小為 %1$s。" "檔案太大,無法上傳" + "最佳化影像品質" + "正在處理……" "封鎖使用者" "檢查您是否要隱藏所有來自此使用者的目前及未來的訊息" "此訊息將會回報給您的家伺服器管理員。他們將無法讀取任何已加密的訊息。" diff --git a/features/messages/impl/src/main/res/values-zh/translations.xml b/features/messages/impl/src/main/res/values-zh/translations.xml index 2e9710ac84..7bbab91d45 100644 --- a/features/messages/impl/src/main/res/values-zh/translations.xml +++ b/features/messages/impl/src/main/res/values-zh/translations.xml @@ -9,8 +9,14 @@ "旅行和地点" "符号" "使用旧版应用程序的用户可能无法看到字幕。" + "点按以更改视频上传质量" + "无法上传该文件。" "处理要上传的媒体失败,请重试。" "上传媒体失败,请重试。" + "允许的最大文件大小为%1$s 。" + "文件太大,无法上传" + "优化图像质量" + "处理中…" "封禁用户" "请确认是否要隐藏该用户当前和未来的所有信息" "此消息将举报给您的服务器管理员。他们无法读取任何加密消息。" @@ -38,12 +44,23 @@ "折叠" "消息已复制" "您无权在此聊天室发言" + + "%1$d 个成员添加表情符号 %2$s" + + + "您与 %1$d 个成员添加表情符号 %2$s" + + "您添加了表情符号%1$s" "折叠" "展开" + "显示反应摘要" "新消息" "%1$d 个聊天室变化" + "跳转至新房间" + "本房间已被替换,现已失效" + "查看历史消息" "该聊天室是其他聊天室的延续" "%1$s,%2$s 和其他 %3$d 个人" diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index dc9e2a1892..7b0827d792 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -7,6 +7,7 @@ "Objects" "Smileys & People" "Travel & Places" + "Recent emojis" "Symbols" "Captions might not be visible to people using older apps." "Tap to change the video upload quality" @@ -15,6 +16,7 @@ "Failed uploading media, please try again." "The maximum file size allowed is %1$s." "The file is too large to upload" + "Item %1$d of %2$d" "Optimise image quality" "Processing…" "Block user" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt new file mode 100644 index 0000000000..a4753807cd --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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 + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.compose.runtime.Composable +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +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.SendLocationEntryPoint +import io.element.android.features.location.api.ShowLocationEntryPoint +import io.element.android.features.location.test.FakeLocationService +import io.element.android.features.messages.api.MessagesEntryPoint +import io.element.android.features.messages.impl.pinned.banner.createPinnedEventsTimelineProvider +import io.element.android.features.messages.impl.timeline.createTimelineController +import io.element.android.features.poll.api.create.CreatePollEntryPoint +import io.element.android.libraries.dateformatter.test.FakeDateFormatter +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +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.timeline.Timeline +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.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +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.MediaViewerEntryPoint +import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme +import io.element.android.libraries.textcomposer.mentions.MentionSpanUpdater +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class DefaultMessagesEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() = runTest { + val entryPoint = DefaultMessagesEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + MessagesFlowNode( + buildContext = buildContext, + plugins = plugins, + matrixClient = FakeMatrixClient(), + sendLocationEntryPoint = object : SendLocationEntryPoint { + override fun builder(timelineMode: Timeline.Mode) = lambdaError() + }, + showLocationEntryPoint = object : ShowLocationEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext, inputs: ShowLocationEntryPoint.Inputs) = lambdaError() + }, + createPollEntryPoint = object : CreatePollEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + elementCallEntryPoint = object : ElementCallEntryPoint { + override fun startCall(callType: CallType) = lambdaError() + override suspend fun handleIncomingCall( + callType: CallType.RoomCall, + eventId: EventId, + senderId: UserId, + roomName: String?, + senderName: String?, + avatarUrl: String?, + timestamp: Long, + expirationTimestamp: Long, + notificationChannelId: String, + textContent: String?, + ) = lambdaError() + }, + mediaViewerEntryPoint = object : MediaViewerEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + analyticsService = FakeAnalyticsService(), + locationService = FakeLocationService(), + room = FakeBaseRoom(), + roomMemberProfilesCache = RoomMemberProfilesCache(), + roomNamesCache = RoomNamesCache(), + mentionSpanUpdater = object : MentionSpanUpdater { + override fun updateMentionSpans(text: CharSequence) = text + + @Composable + override fun rememberMentionSpans(text: CharSequence) = text + }, + mentionSpanTheme = MentionSpanTheme(A_USER_ID), + pinnedEventsTimelineProvider = createPinnedEventsTimelineProvider(), + timelineController = createTimelineController(), + knockRequestsListEntryPoint = object : KnockRequestsListEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + dateFormatter = FakeDateFormatter(), + coroutineDispatchers = testCoroutineDispatchers(), + ) + } + val callback = object : MessagesEntryPoint.Callback { + override fun onRoomDetailsClick() = lambdaError() + override fun onUserDataClick(userId: UserId) = lambdaError() + override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() + override fun onForwardedToSingleRoom(roomId: RoomId) = lambdaError() + } + val initialTarget = MessagesEntryPoint.InitialTarget.Messages(focusedEventId = AN_EVENT_ID) + val params = MessagesEntryPoint.Params(initialTarget) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .callback(callback) + .build() + assertThat(result).isInstanceOf(MessagesFlowNode::class.java) + assertThat(result.plugins).contains(MessagesEntryPoint.Params(initialTarget)) + assertThat(result.plugins).contains(callback) + } + + @Test + fun `test initial target to nav target mapping`() { + assertThat(MessagesEntryPoint.InitialTarget.Messages(focusedEventId = AN_EVENT_ID).toNavTarget()) + .isEqualTo(MessagesFlowNode.NavTarget.Messages(AN_EVENT_ID)) + assertThat(MessagesEntryPoint.InitialTarget.PinnedMessages.toNavTarget()) + .isEqualTo(MessagesFlowNode.NavTarget.PinnedMessagesList) + } +} 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 c90ee16e47..bb59ca8551 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 @@ -21,8 +21,8 @@ class FakeMessagesNavigator( private val onForwardEventClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() }, 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, serverNames: List) -> Unit = { _, _ -> lambdaError() }, + private val onPreviewAttachmentLambda: (attachments: ImmutableList, inReplyToEventId: EventId?) -> Unit = { _, _ -> lambdaError() }, + private val onNavigateToRoomLambda: (roomId: RoomId, threadId: EventId?, serverNames: List) -> Unit = { _, _, _ -> lambdaError() }, private val onOpenThreadLambda: (threadRootId: ThreadId, focusedEventId: EventId?) -> Unit = { _, _ -> lambdaError() }, ) : MessagesNavigator { override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { @@ -41,12 +41,12 @@ class FakeMessagesNavigator( onEditPollClickLambda(eventId) } - override fun onPreviewAttachment(attachments: ImmutableList) { - onPreviewAttachmentLambda(attachments) + override fun onPreviewAttachment(attachments: ImmutableList, inReplyToEventId: EventId?) { + onPreviewAttachmentLambda(attachments, inReplyToEventId) } - override fun onNavigateToRoom(roomId: RoomId, serverNames: List) { - onNavigateToRoomLambda(roomId, serverNames) + override fun onNavigateToRoom(roomId: RoomId, eventId: EventId?, serverNames: List) { + onNavigateToRoomLambda(roomId, eventId, serverNames) } override fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) { 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 b16398635d..56a3badf26 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 @@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMess import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.aTimelineState +import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo 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.TimelineItemTextContent @@ -46,12 +47,17 @@ import io.element.android.libraries.core.mimetype.MimeTypes 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.SnackbarDispatcher +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.core.UserId +import io.element.android.libraries.matrix.api.core.toThreadId 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.recentemojis.AddRecentEmoji 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 @@ -67,8 +73,10 @@ 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_THREAD_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser @@ -154,9 +162,9 @@ class MessagesPresenterTest { @Test fun `present - handle toggling a reaction`() = runTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) - val toggleReactionSuccess = lambdaRecorder { _: String, _: EventOrTransactionId -> Result.success(Unit) } + val toggleReactionSuccess = lambdaRecorder { _: String, _: EventOrTransactionId -> Result.success(true) } val toggleReactionFailure = - lambdaRecorder { _: String, _: EventOrTransactionId -> Result.failure(IllegalStateException("Failed to send reaction")) } + lambdaRecorder { _: String, _: EventOrTransactionId -> Result.failure(IllegalStateException("Failed to send reaction")) } val timeline = FakeTimeline().apply { this.toggleReactionLambda = toggleReactionSuccess @@ -194,7 +202,11 @@ class MessagesPresenterTest { @Test fun `present - handle toggling a reaction twice`() = runTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) - val toggleReactionSuccess = lambdaRecorder { _: String, _: EventOrTransactionId -> Result.success(Unit) } + var toggle = false + val toggleReactionSuccess = lambdaRecorder { _: String, _: EventOrTransactionId -> + toggle = !toggle + Result.success(toggle) + } val timeline = FakeTimeline().apply { this.toggleReactionLambda = toggleReactionSuccess @@ -784,8 +796,8 @@ class MessagesPresenterTest { canUserPinUnpinResult = { Result.success(true) }, canUserSendMessageResult = { _, messageEventType -> when (messageEventType) { - MessageEventType.ROOM_MESSAGE -> Result.success(true) - MessageEventType.REACTION -> Result.success(true) + MessageEventType.RoomMessage -> Result.success(true) + MessageEventType.Reaction -> Result.success(true) else -> lambdaError() } }, @@ -810,8 +822,8 @@ class MessagesPresenterTest { canUserPinUnpinResult = { Result.success(true) }, canUserSendMessageResult = { _, messageEventType -> when (messageEventType) { - MessageEventType.ROOM_MESSAGE -> Result.success(false) - MessageEventType.REACTION -> Result.success(false) + MessageEventType.RoomMessage -> Result.success(false) + MessageEventType.Reaction -> Result.success(false) else -> lambdaError() } }, @@ -1158,6 +1170,74 @@ class MessagesPresenterTest { } } + @Test + fun `present - handle action reply in thread for an event in a thread`() = runTest { + val openThreadLambda = lambdaRecorder { _: ThreadId, _: EventId? -> } + val presenter = createMessagesPresenter( + navigator = FakeMessagesNavigator(onOpenThreadLambda = openThreadLambda), + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.Threads.key to true) + ), + ) + presenter.testWithLifecycleOwner { + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction( + action = TimelineItemAction.ReplyInThread, + event = aMessageEvent(threadInfo = TimelineItemThreadInfo.ThreadResponse(A_THREAD_ID)) + )) + awaitItem() + openThreadLambda.assertions().isCalledOnce().with(value(A_THREAD_ID), value(null)) + } + } + + @Test + fun `present - handle action reply in thread to start a new thread`() = runTest { + val openThreadLambda = lambdaRecorder { _: ThreadId, _: EventId? -> } + val presenter = createMessagesPresenter( + navigator = FakeMessagesNavigator(onOpenThreadLambda = openThreadLambda), + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.Threads.key to true) + ), + ) + presenter.testWithLifecycleOwner { + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction( + action = TimelineItemAction.ReplyInThread, + event = aMessageEvent( + // The event id will be used as the thread id instead + eventId = AN_EVENT_ID, + threadInfo = null, + ) + )) + awaitItem() + openThreadLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID.toThreadId()), value(null)) + } + } + + @Test + fun `present - handle action reply in a thread with threads disabled`() = runTest { + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.Threads.key to false) + ), + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) + presenter.testWithLifecycleOwner { + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ReplyInThread, aMessageEvent())) + awaitItem() + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.Reply( + replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID), + hideImage = false, + ) + ) + ) + } + } + private fun TestScope.createMessagesPresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), joinedRoom: FakeJoinedRoom = FakeJoinedRoom( @@ -1189,7 +1269,9 @@ class MessagesPresenterTest { aRoomMemberModerationState() }, encryptionService: FakeEncryptionService = FakeEncryptionService(), + featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), actionListEventSink: (ActionListEvents) -> Unit = {}, + addRecentEmoji: AddRecentEmoji = AddRecentEmoji(FakeMatrixClient(), testCoroutineDispatchers()), ): MessagesPresenter { return MessagesPresenter( room = joinedRoom, @@ -1217,6 +1299,8 @@ class MessagesPresenterTest { permalinkParser = permalinkParser, encryptionService = encryptionService, analyticsService = analyticsService, + featureFlagService = featureFlagService, + addRecentEmoji = addRecentEmoji, ) } } 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 f8a5436ffe..85027eb75e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -73,6 +73,7 @@ import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.setSafeContent import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -369,6 +370,7 @@ class MessagesViewTest { displayEmojiReactions = true, actions = persistentListOf(TimelineItemAction.Edit), verifiedUserSendFailure = VerifiedUserSendFailure.None, + recentEmojis = persistentListOf(), ) ), ) @@ -461,6 +463,7 @@ class MessagesViewTest { displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf(TimelineItemAction.Edit), + recentEmojis = persistentListOf(), ), ), customReactionState = aCustomReactionState( @@ -490,6 +493,7 @@ class MessagesViewTest { displayEmojiReactions = true, verifiedUserSendFailure = aChangedIdentitySendFailure(), actions = persistentListOf(), + recentEmojis = persistentListOf(), ), ), timelineState = aTimelineState(eventSink = eventsRecorder) @@ -518,13 +522,13 @@ class MessagesViewTest { target = CustomReactionState.Target.Success( event = timelineItem, emojibaseStore = EmojibaseStore( - categories = mapOf( - EmojibaseCategory.People to listOf( + categories = persistentMapOf( + EmojibaseCategory.People to persistentListOf( Emoji( hexcode = "", label = "", - tags = emptyList(), - shortcodes = emptyList(), + tags = persistentListOf(), + shortcodes = persistentListOf(), unicode = aUnicode, skins = null, ) 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 d2d187188c..52118a400d 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 @@ -18,8 +18,9 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent +import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent @@ -27,14 +28,16 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent 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.BaseRoom import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo 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_THREAD_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.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.aRoomInfo @@ -91,7 +94,8 @@ class ActionListPresenterTest { verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.ViewSource, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -132,7 +136,8 @@ class ActionListPresenterTest { verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.ViewSource, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -179,7 +184,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyText, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -195,7 +201,7 @@ class ActionListPresenterTest { val messageEvent = aMessageEvent( isMine = false, isEditable = false, - threadInfo = EventThreadInfo(threadRootId = A_THREAD_ID, threadSummary = null), + threadInfo = TimelineItemThreadInfo.ThreadResponse(threadRootId = A_THREAD_ID), content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) ) initialState.eventSink.invoke( @@ -225,7 +231,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyText, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -271,7 +278,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyText, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -319,7 +327,8 @@ class ActionListPresenterTest { TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -367,7 +376,8 @@ class ActionListPresenterTest { TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -414,7 +424,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyText, TimelineItemAction.ViewSource, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -429,7 +440,7 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, - threadInfo = EventThreadInfo(threadRootId = A_THREAD_ID, threadSummary = null), + threadInfo = TimelineItemThreadInfo.ThreadResponse(threadRootId = A_THREAD_ID), content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) ) initialState.eventSink.invoke( @@ -460,7 +471,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyText, TimelineItemAction.ViewSource, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -506,7 +518,8 @@ class ActionListPresenterTest { TimelineItemAction.Pin, TimelineItemAction.CopyText, TimelineItemAction.ViewSource, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -549,7 +562,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyLink, TimelineItemAction.CopyText, TimelineItemAction.ViewSource, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -596,7 +610,8 @@ class ActionListPresenterTest { TimelineItemAction.Pin, TimelineItemAction.ViewSource, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -647,7 +662,8 @@ class ActionListPresenterTest { TimelineItemAction.RemoveCaption, TimelineItemAction.ViewSource, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -696,7 +712,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyCaption, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -736,7 +753,8 @@ class ActionListPresenterTest { verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.ViewSource, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -809,7 +827,8 @@ class ActionListPresenterTest { TimelineItemAction.Pin, TimelineItemAction.CopyText, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -855,7 +874,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyText, TimelineItemAction.ViewSource, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -908,7 +928,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyText, TimelineItemAction.ViewSource, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -1001,7 +1022,8 @@ class ActionListPresenterTest { TimelineItemAction.Edit, TimelineItemAction.CopyText, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1045,7 +1067,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyLink, TimelineItemAction.Pin, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1088,7 +1111,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyLink, TimelineItemAction.Pin, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1130,7 +1154,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyLink, TimelineItemAction.Pin, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1175,7 +1200,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyLink, TimelineItemAction.Pin, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1190,7 +1216,7 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, - content = TimelineItemCallNotifyContent(), + content = TimelineItemRtcNotificationContent(), ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( @@ -1212,7 +1238,8 @@ class ActionListPresenterTest { verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.ViewSource - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1245,8 +1272,12 @@ class ActionListPresenterTest { } @Test - fun `present - compute for threaded timeline`() = runTest { - val presenter = createActionListPresenter(isDeveloperModeEnabled = false, timelineMode = Timeline.Mode.Thread(A_THREAD_ID)) + fun `present - compute for threaded timeline with threads enabled`() = runTest { + val presenter = createActionListPresenter( + isDeveloperModeEnabled = false, + timelineMode = Timeline.Mode.Thread(A_THREAD_ID), + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.Threads.key to true)), + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -1257,7 +1288,7 @@ class ActionListPresenterTest { content = aTimelineItemVoiceContent( caption = null, ), - threadInfo = EventThreadInfo(A_THREAD_ID, null) + threadInfo = TimelineItemThreadInfo.ThreadResponse(threadRootId = A_THREAD_ID) ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( @@ -1285,9 +1316,171 @@ class ActionListPresenterTest { TimelineItemAction.CopyLink, TimelineItemAction.Pin, TimelineItemAction.Redact, + ), + recentEmojis = persistentListOf(), + ) + ) + } + } + + @Test + fun `present - compute for remote timeline item with threads enabled`() = runTest { + val presenter = createActionListPresenter( + isDeveloperModeEnabled = false, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.Threads.key to true)), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + eventId = AN_EVENT_ID, + isMine = true, + isEditable = false, + content = aTimelineItemVoiceContent( + caption = null, + ), + ) + + assertThat(messageEvent.isRemote).isTrue() + + initialState.eventSink.invoke( + ActionListEvents.ComputeForMessage( + event = messageEvent, + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + canPinUnpin = true ) ) ) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + event = messageEvent, + sentTimeFull = "0 Full true", + displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, + actions = persistentListOf( + TimelineItemAction.Reply, + TimelineItemAction.ReplyInThread, + TimelineItemAction.Forward, + TimelineItemAction.CopyLink, + TimelineItemAction.Pin, + TimelineItemAction.Redact, + ), + recentEmojis = persistentListOf(), + ) + ) + } + } + + @Test + fun `present - compute for remote timeline item already in thread with threads enabled`() = runTest { + val presenter = createActionListPresenter( + isDeveloperModeEnabled = false, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.Threads.key to true)), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + eventId = AN_EVENT_ID, + isMine = true, + isEditable = false, + content = aTimelineItemVoiceContent( + caption = null, + ), + threadInfo = TimelineItemThreadInfo.ThreadResponse(threadRootId = A_THREAD_ID), + ) + + assertThat(messageEvent.isRemote).isTrue() + + initialState.eventSink.invoke( + ActionListEvents.ComputeForMessage( + event = messageEvent, + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + canPinUnpin = true + ) + ) + ) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + event = messageEvent, + sentTimeFull = "0 Full true", + displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, + actions = persistentListOf( + TimelineItemAction.Reply, + TimelineItemAction.ReplyInThread, + TimelineItemAction.Forward, + TimelineItemAction.CopyLink, + TimelineItemAction.Pin, + TimelineItemAction.Redact, + ), + recentEmojis = persistentListOf(), + ) + ) + } + } + + @Test + fun `present - compute for local timeline item with threads enabled`() = runTest { + val presenter = createActionListPresenter( + isDeveloperModeEnabled = false, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.Threads.key to true)), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + eventId = null, + transactionId = A_TRANSACTION_ID, + isMine = true, + isEditable = false, + content = aTimelineItemVoiceContent( + caption = null, + ), + ) + + assertThat(messageEvent.isRemote).isFalse() + + initialState.eventSink.invoke( + ActionListEvents.ComputeForMessage( + event = messageEvent, + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + canPinUnpin = true + ) + ) + ) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + event = messageEvent, + sentTimeFull = "0 Full true", + displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, + actions = persistentListOf( + // Can't reply in thread for local events + TimelineItemAction.Reply, + TimelineItemAction.Redact, + ), + recentEmojis = persistentListOf(), + ) + ) } } } @@ -1296,6 +1489,7 @@ private fun createActionListPresenter( isDeveloperModeEnabled: Boolean, room: BaseRoom = FakeBaseRoom(), timelineMode: Timeline.Mode = Timeline.Mode.Live, + featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), ): ActionListPresenter { val preferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled) return DefaultActionListPresenter( @@ -1305,5 +1499,7 @@ private fun createActionListPresenter( userSendFailureFactory = VerifiedUserSendFailureFactory(room), dateFormatter = FakeDateFormatter(), timelineMode = timelineMode, + featureFlagService = featureFlagService, + getRecentEmojis = { Result.success(persistentListOf()) }, ) } 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 38535f3bdb..941a81d075 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 @@ -58,6 +58,7 @@ 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.mockk.every import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CompletableDeferred @@ -77,7 +78,9 @@ class AttachmentsPreviewPresenterTest { @get:Rule val warmUpRule = WarmUpRule() - private val mockMediaUrl: Uri = mockk("localMediaUri") + private val mockMediaUrl: Uri = mockk("localMediaUri") { + every { path } returns "/path/to/media" + } @Test fun `present - initial state`() = runTest { @@ -615,6 +618,7 @@ class AttachmentsPreviewPresenterTest { dispatchers = testCoroutineDispatchers(), mediaOptimizationSelectorPresenterFactory = mediaOptimizationSelectorPresenterFactory, timelineMode = timelineMode, + inReplyToEventId = null, ) } 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 a9f2e075ac..703b4b0043 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 @@ -12,6 +12,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.model.ReadReceiptData import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts +import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -19,7 +20,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UniqueId -import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.MessageShieldProvider import io.element.android.libraries.matrix.api.timeline.item.event.SendHandleProvider @@ -41,7 +41,7 @@ internal fun aMessageEvent( canBeRepliedTo: Boolean = true, content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, formattedBody = A_MESSAGE, isEdited = false), inReplyTo: InReplyToDetails? = null, - threadInfo: EventThreadInfo = EventThreadInfo(threadRootId = null, threadSummary = null), + threadInfo: TimelineItemThreadInfo? = null, sendState: LocalEventSendState = LocalEventSendState.Sent(AN_EVENT_ID), debugInfoProvider: TimelineItemDebugInfoProvider = TimelineItemDebugInfoProvider { aTimelineItemDebugInfo() }, messageShieldProvider: MessageShieldProvider = MessageShieldProvider { null }, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt index 2cf0f8a3c9..539618df9f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt @@ -7,6 +7,7 @@ package io.element.android.features.messages.impl.fixtures +import io.element.android.features.messages.impl.messagesummary.FakeMessageSummaryFormatter import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory @@ -30,7 +31,8 @@ import io.element.android.features.poll.test.pollcontent.FakePollContentStateFac import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.eventformatter.api.TimelineEventFormatter -import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation @@ -75,11 +77,13 @@ internal fun TestScope.aTimelineItemsFactory( stateFactory = TimelineItemContentStateFactory(timelineEventFormatter), failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(), failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(), + sessionId = matrixClient.sessionId, ), matrixClient = matrixClient, dateFormatter = FakeDateFormatter(), permalinkParser = FakePermalinkParser(), - config = config + config = config, + summaryFormatter = FakeMessageSummaryFormatter(), ) } }, @@ -95,7 +99,7 @@ internal fun TestScope.aTimelineItemsFactory( internal fun aTimelineEventFormatter(): TimelineEventFormatter { return object : TimelineEventFormatter { - override fun format(event: EventTimelineItem): CharSequence { + override fun format(content: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): CharSequence? { return "" } } 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 84aeb2ce28..735fdaaabe 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 @@ -688,7 +688,7 @@ class MessageComposerPresenterTest { val room = FakeJoinedRoom( typingNoticeResult = { Result.success(Unit) } ) - val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList -> } + val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList, _: EventId? -> } val navigator = FakeMessagesNavigator( onPreviewAttachmentLambda = onPreviewAttachmentLambda ) @@ -728,7 +728,7 @@ class MessageComposerPresenterTest { val room = FakeJoinedRoom( typingNoticeResult = { Result.success(Unit) } ) - val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList -> } + val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList, _: EventId? -> } val navigator = FakeMessagesNavigator( onPreviewAttachmentLambda = onPreviewAttachmentLambda ) @@ -785,7 +785,7 @@ class MessageComposerPresenterTest { val room = FakeJoinedRoom( typingNoticeResult = { Result.success(Unit) } ) - val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList -> } + val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList, _: EventId? -> } val navigator = FakeMessagesNavigator( onPreviewAttachmentLambda = onPreviewAttachmentLambda ) @@ -846,7 +846,7 @@ class MessageComposerPresenterTest { typingNoticeResult = { Result.success(Unit) } ) val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() } - val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList -> } + val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList, _: EventId? -> } val navigator = FakeMessagesNavigator( onPreviewAttachmentLambda = onPreviewAttachmentLambda ) @@ -870,7 +870,7 @@ class MessageComposerPresenterTest { typingNoticeResult = { Result.success(Unit) } ) val permissionPresenter = FakePermissionsPresenter() - val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList -> } + val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList, _: EventId? -> } val navigator = FakeMessagesNavigator( onPreviewAttachmentLambda = onPreviewAttachmentLambda ) @@ -896,7 +896,7 @@ class MessageComposerPresenterTest { typingNoticeResult = { Result.success(Unit) } ) val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() } - val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList -> } + val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList, _: EventId? -> } val navigator = FakeMessagesNavigator( onPreviewAttachmentLambda = onPreviewAttachmentLambda ) @@ -920,7 +920,7 @@ class MessageComposerPresenterTest { typingNoticeResult = { Result.success(Unit) } ) val permissionPresenter = FakePermissionsPresenter() - val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList -> } + val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList, _: EventId? -> } val navigator = FakeMessagesNavigator( onPreviewAttachmentLambda = onPreviewAttachmentLambda ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagesummary/FakeMessageSummaryFormatter.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagesummary/FakeMessageSummaryFormatter.kt index 315f2e3a7a..06d1cca1c7 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagesummary/FakeMessageSummaryFormatter.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagesummary/FakeMessageSummaryFormatter.kt @@ -7,13 +7,13 @@ package io.element.android.features.messages.impl.messagesummary -import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter class FakeMessageSummaryFormatter : MessageSummaryFormatter { private var result = "A message" - override fun format(event: TimelineItem.Event): String = result + override fun format(content: TimelineItemEventContent): String = result fun givenMessageResult(value: String) { result = value 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 4840d5bed1..38182dec1d 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 @@ -178,10 +178,9 @@ class PinnedMessagesBannerPresenterTest { ), syncService: SyncService = FakeSyncService(), ): PinnedMessagesBannerPresenter { - val timelineProvider = PinnedEventsTimelineProvider( + val timelineProvider = createPinnedEventsTimelineProvider( room = room, syncService = syncService, - dispatchers = testCoroutineDispatchers(), ) timelineProvider.launchIn(backgroundScope) @@ -192,3 +191,12 @@ class PinnedMessagesBannerPresenterTest { ) } } + +internal fun TestScope.createPinnedEventsTimelineProvider( + room: JoinedRoom = FakeJoinedRoom(), + syncService: SyncService = FakeSyncService(), +) = PinnedEventsTimelineProvider( + room = room, + syncService = syncService, + dispatchers = testCoroutineDispatchers(), +) 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 a2005b7a39..524cb3e1e8 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 @@ -40,7 +40,7 @@ class TimelineControllerTest { assertThat(state).isEqualTo(liveTimeline) } assertThat(sut.isLive().first()).isTrue() - sut.focusOnEvent(AN_EVENT_ID) + sut.focusOnEvent(AN_EVENT_ID, null) awaitItem().also { state -> assertThat(state).isEqualTo(detachedTimeline) } @@ -78,14 +78,14 @@ class TimelineControllerTest { awaitItem().also { state -> assertThat(state).isEqualTo(liveTimeline) } - sut.focusOnEvent(AN_EVENT_ID) + sut.focusOnEvent(AN_EVENT_ID, null) awaitItem().also { state -> assertThat(state).isEqualTo(detachedTimeline1) } assertThat(detachedTimeline1.closeCounter).isEqualTo(0) assertThat(detachedTimeline2.closeCounter).isEqualTo(0) // Focus on another event should close the previous detached timeline - sut.focusOnEvent(AN_EVENT_ID) + sut.focusOnEvent(AN_EVENT_ID, null) awaitItem().also { state -> assertThat(state).isEqualTo(detachedTimeline2) } @@ -124,7 +124,7 @@ class TimelineControllerTest { awaitItem().also { state -> assertThat(state).isEqualTo(liveTimeline) } - sut.focusOnEvent(AN_EVENT_ID) + sut.focusOnEvent(AN_EVENT_ID, null) awaitItem().also { state -> assertThat(state).isEqualTo(detachedTimeline) } @@ -171,11 +171,11 @@ class TimelineControllerTest { ) val sut = TimelineController(room = joinedRoom, liveTimeline = liveTimeline) sut.activeTimelineFlow().test { - sut.focusOnEvent(AN_EVENT_ID) + sut.focusOnEvent(AN_EVENT_ID, null) awaitItem().also { state -> assertThat(state).isEqualTo(liveTimeline) } - sut.focusOnEvent(AN_EVENT_ID) + sut.focusOnEvent(AN_EVENT_ID, null) awaitItem().also { state -> assertThat(state).isEqualTo(detachedTimeline) } @@ -200,7 +200,7 @@ class TimelineControllerTest { awaitItem().also { state -> assertThat(state).isEqualTo(liveTimeline) } - sut.focusOnEvent(AN_EVENT_ID) + sut.focusOnEvent(AN_EVENT_ID, null) awaitItem().also { state -> assertThat(state).isEqualTo(detachedTimeline) } @@ -217,3 +217,13 @@ class TimelineControllerTest { } } } + +internal fun createTimelineController( + room: FakeJoinedRoom = FakeJoinedRoom(liveTimeline = FakeTimeline()), + liveTimeline: Timeline = FakeTimeline(name = "live"), +): TimelineController { + return TimelineController( + room = room, + liveTimeline = liveTimeline + ) +} 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 8710af8bda..8da614f67e 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 @@ -31,7 +31,9 @@ import io.element.android.features.roomcall.api.aStandByCallState 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.core.UniqueId +import io.element.android.libraries.matrix.api.core.asEventId 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 @@ -44,6 +46,8 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime 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_THREAD_ID +import io.element.android.libraries.matrix.test.A_THREAD_ID_2 import io.element.android.libraries.matrix.test.A_UNIQUE_ID import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID @@ -535,7 +539,10 @@ class TimelinePresenterTest { val room = FakeJoinedRoom( liveTimeline = liveTimeline, createTimelineResult = { Result.success(detachedTimeline) }, - baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) }), + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + threadRootIdForEventResult = { _ -> Result.success(null) }, + ), ) val presenter = createTimelinePresenter( room = room, @@ -613,7 +620,10 @@ class TimelinePresenterTest { timelineItems = flowOf(emptyList()), ), createTimelineResult = { Result.failure(RuntimeException("An error")) }, - baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) }), + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + threadRootIdForEventResult = { _ -> Result.success(null) }, + ), ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -639,6 +649,246 @@ class TimelinePresenterTest { } } + @Test + fun `present - focus on event in a thread opens the thread`() = runTest { + val threadId = A_THREAD_ID + val detachedTimeline = FakeTimeline( + mode = Timeline.Mode.FocusedOnEvent(AN_EVENT_ID_2), + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event( + uniqueId = A_UNIQUE_ID, + event = anEventTimelineItem(), + ) + ) + ) + ) + val liveTimeline = FakeTimeline( + timelineItems = flowOf(emptyList()) + ) + val room = FakeJoinedRoom( + liveTimeline = liveTimeline, + createTimelineResult = { Result.success(detachedTimeline) }, + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + threadRootIdForEventResult = { _ -> Result.success(threadId) }, + ), + ) + val openThreadLambda = lambdaRecorder { _: ThreadId, _: EventId? -> } + val navigator = FakeMessagesNavigator(onOpenThreadLambda = openThreadLambda) + val presenter = createTimelinePresenter( + room = room, + timeline = liveTimeline, + messagesNavigator = navigator, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) + + awaitItem().also { state -> + assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) + assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Requested(AN_EVENT_ID, Duration.ZERO)) + } + + advanceUntilIdle() + + assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID)) + + // The live timeline focuses in the thread root + assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.Success(A_THREAD_ID.asEventId())) + + // The thread is opened + openThreadLambda.assertions() + .isCalledOnce() + .with( + value(threadId), + value(AN_EVENT_ID), + ) + } + } + + @Test + fun `present - focus on event in a thread when in the same thread just moves the focus`() = runTest { + val threadId = A_THREAD_ID + val detachedTimeline = FakeTimeline( + mode = Timeline.Mode.FocusedOnEvent(AN_EVENT_ID_2), + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event( + uniqueId = A_UNIQUE_ID, + event = anEventTimelineItem(), + ) + ) + ) + ) + val liveTimeline = FakeTimeline( + mode = Timeline.Mode.Thread(threadId), + timelineItems = flowOf(emptyList()) + ) + val room = FakeJoinedRoom( + liveTimeline = liveTimeline, + createTimelineResult = { Result.success(detachedTimeline) }, + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + threadRootIdForEventResult = { _ -> Result.success(threadId) }, + ), + ) + val openThreadLambda = lambdaRecorder { _: ThreadId, _: EventId? -> } + val navigator = FakeMessagesNavigator(onOpenThreadLambda = openThreadLambda) + val presenter = createTimelinePresenter( + room = room, + timeline = liveTimeline, + messagesNavigator = navigator, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) + + awaitItem().also { state -> + assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) + assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Requested(AN_EVENT_ID, Duration.ZERO)) + } + + advanceUntilIdle() + + assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID)) + + // The live timeline focuses in the event directly since we are already in the thread + assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.Success(AN_EVENT_ID)) + + // The thread is not opened again + openThreadLambda.assertions().isNeverCalled() + } + } + + @Test + fun `present - focus on event in a thread when in a different thread opens the new thread`() = runTest { + val currentThreadId = A_THREAD_ID + val detachedTimeline = FakeTimeline( + mode = Timeline.Mode.FocusedOnEvent(AN_EVENT_ID_2), + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event( + uniqueId = A_UNIQUE_ID, + event = anEventTimelineItem(), + ) + ) + ) + ) + val liveTimeline = FakeTimeline( + mode = Timeline.Mode.Thread(currentThreadId), + timelineItems = flowOf(emptyList()) + ) + val room = FakeJoinedRoom( + liveTimeline = liveTimeline, + createTimelineResult = { Result.success(detachedTimeline) }, + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + // Use a different thread id + threadRootIdForEventResult = { _ -> Result.success(A_THREAD_ID_2) }, + ), + ) + val openThreadLambda = lambdaRecorder { _: ThreadId, _: EventId? -> } + val navigator = FakeMessagesNavigator(onOpenThreadLambda = openThreadLambda) + val presenter = createTimelinePresenter( + room = room, + timeline = liveTimeline, + messagesNavigator = navigator, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) + + awaitItem().also { state -> + assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) + assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Requested(AN_EVENT_ID, Duration.ZERO)) + } + + advanceUntilIdle() + + assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID)) + + // The live timeline focuses in the event directly since we are already in the thread + assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.Success(A_THREAD_ID_2.asEventId())) + + // The other thread is opened + openThreadLambda.assertions() + .isCalledOnce() + .with( + value(A_THREAD_ID_2), + value(AN_EVENT_ID), + ) + } + } + + @Test + fun `present - focus on event in a the room while in a thread of that room opens the room`() = runTest { + val detachedTimeline = FakeTimeline( + mode = Timeline.Mode.FocusedOnEvent(AN_EVENT_ID_2), + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event( + uniqueId = A_UNIQUE_ID, + event = anEventTimelineItem(), + ) + ) + ) + ) + val liveTimeline = FakeTimeline( + mode = Timeline.Mode.Thread(A_THREAD_ID), + timelineItems = flowOf(emptyList()) + ) + val room = FakeJoinedRoom( + liveTimeline = liveTimeline, + createTimelineResult = { Result.success(detachedTimeline) }, + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + // The event is in the main timeline, not in a thread + threadRootIdForEventResult = { _ -> Result.success(null) }, + ), + ) + val openRoomLambda = lambdaRecorder { _: RoomId, _: EventId?, _: List -> } + val navigator = FakeMessagesNavigator(onNavigateToRoomLambda = openRoomLambda) + val presenter = createTimelinePresenter( + room = room, + timeline = liveTimeline, + messagesNavigator = navigator, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) + + awaitItem().also { state -> + assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) + assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Requested(AN_EVENT_ID, Duration.ZERO)) + } + + advanceUntilIdle() + + assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID)) + + // The focus state will reset + assertThat(awaitItem().focusRequestState).isEqualTo(FocusRequestState.None) + + // The room is opened again + openRoomLambda.assertions() + .isCalledOnce() + .with( + value(room.roomId), + value(AN_EVENT_ID), + value(emptyList()) + ) + } + } + @Test fun `present - show shield hide shield`() = runTest { val presenter = createTimelinePresenter() @@ -754,7 +1004,7 @@ class TimelinePresenterTest { canUserSendMessageResult = { _, _ -> Result.success(true) }, ), ) - val onNavigateToRoomLambda = lambdaRecorder, Unit> { _, _ -> } + val onNavigateToRoomLambda = lambdaRecorder, Unit> { _, _, _ -> } val navigator = FakeMessagesNavigator( onNavigateToRoomLambda = onNavigateToRoomLambda ) @@ -766,6 +1016,8 @@ class TimelinePresenterTest { .isCalledOnce() .with( value(A_ROOM_ID), + // No event id when navigating to a successor/predecessor room + value(null), value(emptyList()) ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenterTest.kt index 034d5aa3a8..e34bbdcbef 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenterTest.kt @@ -23,7 +23,10 @@ class CustomReactionPresenterTest { @get:Rule val warmUpRule = WarmUpRule() - private val presenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) + private val presenter = CustomReactionPresenter( + emojibaseProvider = FakeEmojibaseProvider(), + getRecentEmojis = { Result.success(emptyList()) }, + ) @Test fun `present - handle selecting and de-selecting an event`() = runTest { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/FakeEmojibaseProvider.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/FakeEmojibaseProvider.kt index 3cba1b6cf3..498027bae6 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/FakeEmojibaseProvider.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/FakeEmojibaseProvider.kt @@ -8,8 +8,9 @@ package io.element.android.features.messages.impl.timeline.components.customreaction import io.element.android.emojibasebindings.EmojibaseStore +import kotlinx.collections.immutable.persistentMapOf class FakeEmojibaseProvider : EmojibaseProvider { override val emojibaseStore: EmojibaseStore - get() = EmojibaseStore(mapOf()) + get() = EmojibaseStore(persistentMapOf()) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt new file mode 100644 index 0000000000..74f72a9c31 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.components.customreaction.picker + +import androidx.compose.runtime.InternalComposeApi +import androidx.compose.runtime.currentComposer +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.TurbineTestContext +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.emojibasebindings.Emoji +import io.element.android.emojibasebindings.EmojibaseCategory +import io.element.android.emojibasebindings.EmojibaseStore +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentMap +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class EmojiPickerPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `UpdateSearchQuery loads new results`() = runTest { + testPresenter { + skipItems(1) + + val initialState = awaitItem() + assertThat(initialState.searchQuery).isEmpty() + assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) + + initialState.eventSink(EmojiPickerEvents.UpdateSearchQuery("smile")) + assertThat(awaitItem().searchQuery).isEqualTo("smile") + + val stateWithResults = awaitItem() + assertThat(stateWithResults.searchQuery).isEqualTo("smile") + assertThat(stateWithResults.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) + } + } + + @Test + fun `ToggleSearchActive toggles the search state`() = runTest { + testPresenter { + skipItems(1) + + val initialState = awaitItem() + assertThat(initialState.isSearchActive).isFalse() + + initialState.eventSink(EmojiPickerEvents.ToggleSearchActive(true)) + assertThat(awaitItem().isSearchActive).isTrue() + + initialState.eventSink(EmojiPickerEvents.ToggleSearchActive(false)) + assertThat(awaitItem().isSearchActive).isFalse() + } + } + + @Test + fun `recent emojis are automatically added to the categories if present`() = runTest { + val providedCategories = persistentListOf(emojiCategory(EmojibaseCategory.Activity)) + val presenter = createPresenter( + categories = providedCategories, + recentEmojis = persistentListOf("😊"), + ) + testPresenter(presenter) { + skipItems(1) + + val initialState = awaitItem() + assertThat(providedCategories.size).isNotEqualTo(initialState.categories.size) + assertThat(initialState.categories.size).isEqualTo(2) + } + } + + private fun TestScope.createPresenter( + categories: ImmutableList>> = persistentListOf(emojiCategory()), + recentEmojis: ImmutableList = persistentListOf(), + ) = EmojiPickerPresenter( + emojibaseStore = EmojibaseStore(categories.toMap().toPersistentMap()), + recentEmojis = recentEmojis, + coroutineDispatchers = testCoroutineDispatchers(), + ) + + private fun emojiCategory( + category: EmojibaseCategory = EmojibaseCategory.Activity, + emojis: ImmutableList = persistentListOf( + Emoji("1F3C3", "Smile", persistentListOf("smile"), persistentListOf("smile"), "😊", skins = null) + ) + ) = category to emojis + + @OptIn(InternalComposeApi::class) + private suspend fun TestScope.testPresenter( + presenter: EmojiPickerPresenter = createPresenter(), + testBlock: suspend TurbineTestContext.() -> Unit, + ) { + moleculeFlow(RecompositionMode.Immediate) { + // These are needed to load the history icon in the presenter + currentComposer.startProviders(arrayOf( + LocalContext provides InstrumentationRegistry.getInstrumentation().context, + LocalConfiguration provides InstrumentationRegistry.getInstrumentation().context.resources.configuration, + )) + val state = presenter.present() + currentComposer.endProviders() + state + }.test { + testBlock() + } + } +} 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 917683220e..fb13103694 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 @@ -750,7 +750,7 @@ class TimelineItemContentMessageFactoryTest { body: String = "Body", inReplyTo: InReplyTo? = null, isEdited: Boolean = false, - threadInfo: EventThreadInfo = EventThreadInfo(threadRootId = null, threadSummary = null), + threadInfo: EventThreadInfo? = null, type: MessageType, ): MessageContent { return MessageContent( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt index 188c5f0bcd..1bdc43ebef 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt @@ -18,7 +18,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel import io.element.android.libraries.designsystem.components.avatar.anAvatarData import io.element.android.libraries.matrix.api.core.UniqueId -import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo 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_USER_ID @@ -42,7 +41,7 @@ class TimelineItemGrouperTest { isEditable = false, canBeRepliedTo = false, inReplyTo = null, - threadInfo = EventThreadInfo(threadRootId = null, threadSummary = null), + threadInfo = null, origin = null, timelineItemDebugInfoProvider = { aTimelineItemDebugInfo() }, messageShieldProvider = { null }, diff --git a/features/migration/impl/build.gradle.kts b/features/migration/impl/build.gradle.kts index 878cf40d3a..dd445624b9 100644 --- a/features/migration/impl/build.gradle.kts +++ b/features/migration/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2024 New Vector Ltd. @@ -15,11 +16,12 @@ android { namespace = "io.element.android.features.migration.impl" } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.features.migration.api) implementation(projects.libraries.architecture) + implementation(projects.libraries.androidutils) implementation(projects.libraries.preferences.impl) implementation(libs.androidx.datastore.preferences) implementation(projects.features.rageshake.api) @@ -28,16 +30,9 @@ dependencies { implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.uiStrings) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.sessionStorage.implMemory) testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.preferences.test) - testImplementation(projects.tests.testutils) testImplementation(projects.features.rageshake.test) } diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationEntryPoint.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationEntryPoint.kt index a550d86ada..68c4c7ce3a 100644 --- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationEntryPoint.kt +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationEntryPoint.kt @@ -9,14 +9,15 @@ package io.element.android.features.migration.impl import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.api.MigrationEntryPoint import io.element.android.features.api.MigrationState -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultMigrationEntryPoint @Inject constructor( +@Inject +class DefaultMigrationEntryPoint( private val migrationPresenter: MigrationPresenter, ) : MigrationEntryPoint { @Composable diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt index a79a073022..78883a4e03 100644 --- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt @@ -7,27 +7,23 @@ package io.element.android.features.migration.impl -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject -private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_migration") private val applicationMigrationVersion = intPreferencesKey("applicationMigrationVersion") @ContributesBinding(AppScope::class) -class DefaultMigrationStore @Inject constructor( - @ApplicationContext context: Context, +@Inject +class DefaultMigrationStore( + preferenceDataStoreFactory: PreferenceDataStoreFactory, ) : MigrationStore { - private val store = context.dataStore + private val store = preferenceDataStoreFactory.create("elementx_migration") override suspend fun setApplicationMigrationVersion(version: Int) { store.edit { prefs -> 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 3d8fc3a0aa..7edbc5389f 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 @@ -14,17 +14,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.api.MigrationState import io.element.android.features.migration.impl.migrations.AppMigration import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn import timber.log.Timber -import javax.inject.Inject @SingleIn(AppScope::class) -class MigrationPresenter @Inject constructor( +@Inject +class MigrationPresenter( private val migrationStore: MigrationStore, migrations: Set<@JvmSuppressWildcards AppMigration>, ) : Presenter { diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration01.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration01.kt index 5a851e0b16..048a400f6c 100644 --- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration01.kt +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration01.kt @@ -7,16 +7,17 @@ package io.element.android.features.migration.impl.migrations -import com.squareup.anvil.annotations.ContributesMultibinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.features.rageshake.api.logs.LogFilesRemover -import io.element.android.libraries.di.AppScope -import javax.inject.Inject /** * Remove existing logs from the device to remove any leaks of sensitive data. */ -@ContributesMultibinding(AppScope::class) -class AppMigration01 @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class AppMigration01( private val logFilesRemover: LogFilesRemover, ) : AppMigration { override val order: Int = 1 diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration02.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration02.kt index 7557339fe9..44f4806c65 100644 --- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration02.kt +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration02.kt @@ -7,20 +7,21 @@ package io.element.android.features.migration.impl.migrations -import com.squareup.anvil.annotations.ContributesMultibinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.coroutineScope -import javax.inject.Inject /** * This migration sets the skip session verification preference to true for all existing sessions. * This way we don't force existing users to verify their session again. */ -@ContributesMultibinding(AppScope::class) -class AppMigration02 @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class AppMigration02( private val sessionStore: SessionStore, private val sessionPreferenceStoreFactory: SessionPreferencesStoreFactory, ) : AppMigration { diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration03.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration03.kt index d7e85f7de6..0cb3573954 100644 --- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration03.kt +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration03.kt @@ -7,15 +7,16 @@ package io.element.android.features.migration.impl.migrations -import com.squareup.anvil.annotations.ContributesMultibinding -import io.element.android.libraries.di.AppScope -import javax.inject.Inject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject /** * This performs the same operation as [AppMigration01], since we need to clear the local logs again. */ -@ContributesMultibinding(AppScope::class) -class AppMigration03 @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class AppMigration03( private val migration01: AppMigration01, ) : AppMigration { override val order: Int = 3 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 6023663dac..8ab4921038 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 @@ -8,17 +8,18 @@ package io.element.android.features.migration.impl.migrations import android.content.Context -import com.squareup.anvil.annotations.ContributesMultibinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject 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 +import io.element.android.libraries.di.annotations.ApplicationContext /** * Remove notifications.bin file, used to store notification data locally. */ -@ContributesMultibinding(AppScope::class) -class AppMigration04 @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class AppMigration04( @ApplicationContext private val context: Context, ) : AppMigration { companion object { diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05.kt index 2046df315e..109ff7e0b7 100644 --- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05.kt +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05.kt @@ -7,16 +7,18 @@ package io.element.android.features.migration.impl.migrations -import com.squareup.anvil.annotations.ContributesMultibinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.BaseDirectory import io.element.android.libraries.sessionstorage.api.SessionStore import java.io.File -import javax.inject.Inject -@ContributesMultibinding(AppScope::class) -class AppMigration05 @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class AppMigration05( private val sessionStore: SessionStore, - private val baseDirectory: File, + @BaseDirectory private val baseDirectory: File, ) : AppMigration { override val order: Int = 5 diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06.kt index 618b42dad6..2eb98b9e5f 100644 --- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06.kt +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06.kt @@ -7,18 +7,19 @@ package io.element.android.features.migration.impl.migrations -import com.squareup.anvil.annotations.ContributesMultibinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.sessionstorage.api.SessionStore import java.io.File -import javax.inject.Inject /** * Create the cache directory for the existing sessions. */ -@ContributesMultibinding(AppScope::class) -class AppMigration06 @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class AppMigration06( private val sessionStore: SessionStore, @CacheDirectory private val cacheDirectory: File, ) : AppMigration { diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration07.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration07.kt index 63e2fcc16a..fe88817796 100644 --- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration07.kt +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration07.kt @@ -7,16 +7,17 @@ package io.element.android.features.migration.impl.migrations -import com.squareup.anvil.annotations.ContributesMultibinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.features.rageshake.api.logs.LogFilesRemover -import io.element.android.libraries.di.AppScope -import javax.inject.Inject /** * Delete the previous log files. */ -@ContributesMultibinding(AppScope::class) -class AppMigration07 @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class AppMigration07( private val logFilesRemover: LogFilesRemover, ) : AppMigration { override val order: Int = 7 diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration02Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration02Test.kt index db4986febf..fb7dbf5281 100644 --- a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration02Test.kt +++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration02Test.kt @@ -10,7 +10,7 @@ package io.element.android.features.migration.impl.migrations import com.google.common.truth.Truth.assertThat import io.element.android.libraries.preferences.test.FakeSessionPreferencesStoreFactory import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore -import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.flow.first @@ -20,12 +20,12 @@ import org.junit.Test class AppMigration02Test { @Test fun `test migration`() = runTest { - val sessionStore = InMemorySessionStore().apply { - updateData(aSessionData()) - } + val sessionStore = InMemorySessionStore( + initialList = listOf(aSessionData()), + ) val sessionPreferencesStore = InMemorySessionPreferencesStore(isSessionVerificationSkipped = false) val sessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory( - getLambda = lambdaRecorder { _, _, -> sessionPreferencesStore }, + getLambda = lambdaRecorder { _, _ -> sessionPreferencesStore }, removeLambda = lambdaRecorder { _ -> } ) val migration = AppMigration02(sessionStore = sessionStore, sessionPreferenceStoreFactory = sessionPreferencesStoreFactory) diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05Test.kt index af5f75dd04..af71905635 100644 --- a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05Test.kt +++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05Test.kt @@ -9,7 +9,7 @@ package io.element.android.features.migration.impl.migrations import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData import kotlinx.coroutines.test.runTest import org.junit.Test @@ -18,14 +18,14 @@ import java.io.File class AppMigration05Test { @Test fun `empty session path should be set to an expected path`() = runTest { - val sessionStore = InMemorySessionStore().apply { - updateData( + val sessionStore = InMemorySessionStore( + initialList = listOf( aSessionData( sessionId = A_SESSION_ID.value, sessionPath = "", ) ) - } + ) val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path")) migration.migrate() val storedData = sessionStore.getSession(A_SESSION_ID.value)!! @@ -34,14 +34,14 @@ class AppMigration05Test { @Test fun `non empty session path should not be impacted by the migration`() = runTest { - val sessionStore = InMemorySessionStore().apply { - updateData( + val sessionStore = InMemorySessionStore( + initialList = listOf( aSessionData( sessionId = A_SESSION_ID.value, sessionPath = "/a/path/existing", ) ) - } + ) val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path")) migration.migrate() val storedData = sessionStore.getSession(A_SESSION_ID.value)!! diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06Test.kt index bab0eaee4e..095085cd17 100644 --- a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06Test.kt +++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06Test.kt @@ -9,7 +9,7 @@ package io.element.android.features.migration.impl.migrations import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData import kotlinx.coroutines.test.runTest import org.junit.Test @@ -18,15 +18,15 @@ import java.io.File class AppMigration06Test { @Test fun `empty cache path should be set to an expected path`() = runTest { - val sessionStore = InMemorySessionStore().apply { - updateData( + val sessionStore = InMemorySessionStore( + initialList = listOf( aSessionData( sessionId = A_SESSION_ID.value, sessionPath = "/a/path/to/a/session/AN_ID", cachePath = "", ) ) - } + ) val migration = AppMigration06(sessionStore = sessionStore, cacheDirectory = File("/a/path/cache")) migration.migrate() val storedData = sessionStore.getSession(A_SESSION_ID.value)!! @@ -35,14 +35,14 @@ class AppMigration06Test { @Test fun `non empty cache path should not be impacted by the migration`() = runTest { - val sessionStore = InMemorySessionStore().apply { - updateData( + val sessionStore = InMemorySessionStore( + initialList = listOf( aSessionData( sessionId = A_SESSION_ID.value, cachePath = "/a/path/existing", ) ) - } + ) val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path/cache")) migration.migrate() val storedData = sessionStore.getSession(A_SESSION_ID.value)!! diff --git a/features/networkmonitor/impl/build.gradle.kts b/features/networkmonitor/impl/build.gradle.kts index 076cc8944d..1c070e66df 100644 --- a/features/networkmonitor/impl/build.gradle.kts +++ b/features/networkmonitor/impl/build.gradle.kts @@ -1,4 +1,4 @@ -import extension.setupAnvil +import extension.setupDependencyInjection /* * Copyright 2022-2024 New Vector Ltd. @@ -11,7 +11,7 @@ plugins { id("io.element.android-library") } -setupAnvil() +setupDependencyInjection() android { namespace = "io.element.android.features.networkmonitor.impl" @@ -19,7 +19,6 @@ android { dependencies { implementation(libs.coroutines.core) - implementation(libs.dagger) implementation(projects.libraries.core) implementation(projects.libraries.di) api(projects.features.networkmonitor.api) 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 a69cf4bcbc..76525ff657 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 @@ -13,13 +13,14 @@ import android.content.Context import android.net.ConnectivityManager import android.net.Network import android.net.NetworkRequest -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.networkmonitor.api.NetworkMonitor 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 io.element.android.libraries.di.annotations.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.awaitClose @@ -33,11 +34,11 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import timber.log.Timber import java.util.concurrent.atomic.AtomicInteger -import javax.inject.Inject @ContributesBinding(scope = AppScope::class) @SingleIn(AppScope::class) -class DefaultNetworkMonitor @Inject constructor( +@Inject +class DefaultNetworkMonitor( @ApplicationContext context: Context, @AppCoroutineScope appCoroutineScope: CoroutineScope, diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/history/PollHistoryEntryPoint.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/history/PollHistoryEntryPoint.kt index 7ca61340c4..49f5f92b87 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/history/PollHistoryEntryPoint.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/history/PollHistoryEntryPoint.kt @@ -7,10 +7,6 @@ package io.element.android.features.poll.api.history -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.architecture.SimpleFeatureEntryPoint -interface PollHistoryEntryPoint : FeatureEntryPoint { - fun createNode(parentNode: Node, buildContext: BuildContext): Node -} +interface PollHistoryEntryPoint : SimpleFeatureEntryPoint diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFactory.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFactory.kt index 5229534cc4..e42bb98bef 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFactory.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFactory.kt @@ -7,9 +7,18 @@ package io.element.android.features.poll.api.pollcontent +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.PollContent interface PollContentStateFactory { - suspend fun create(event: EventTimelineItem, content: PollContent): PollContentState + suspend fun create(eventTimelineItem: EventTimelineItem, content: PollContent): PollContentState { + return create( + eventId = eventTimelineItem.eventId, + isEditable = eventTimelineItem.isEditable, + isOwn = eventTimelineItem.isOwn, + content = content, + ) + } + suspend fun create(eventId: EventId?, isEditable: Boolean, isOwn: Boolean, content: PollContent): PollContentState } diff --git a/features/poll/api/src/main/res/values-cy/translations.xml b/features/poll/api/src/main/res/values-cy/translations.xml index 3479c30674..207b2f5aeb 100644 --- a/features/poll/api/src/main/res/values-cy/translations.xml +++ b/features/poll/api/src/main/res/values-cy/translations.xml @@ -1,5 +1,13 @@ + + "%1$d y cant o\'r holl bleidleisiau" + "%1$d y cant o\'r holl bleidleisiau" + "%1$d y cant o\'r holl bleidleisiau" + "%1$d y cant o\'r holl bleidleisiau" + "%1$d y cant o\'r holl bleidleisiau" + "%1$d y cant o\'r holl bleidleisiau" + "Bydd yn dileu\'r dewis blaenorol" "Dyma\'r ateb buddugol" diff --git a/features/poll/api/src/main/res/values-de/translations.xml b/features/poll/api/src/main/res/values-de/translations.xml index eefa8a3814..cce5484a53 100644 --- a/features/poll/api/src/main/res/values-de/translations.xml +++ b/features/poll/api/src/main/res/values-de/translations.xml @@ -1,9 +1,9 @@ - "%1$d Prozent der Stimmen insgesamt" - "%1$d Prozent der Gesamtstimmen" + "%1$d Prozent aller Stimmen" + "%1$d Prozent aller Stimmen" "Entfernt die vorherige Auswahl" - "Das ist die Gewinnerantwort" + "Das ist die meistgewählte Antwort" diff --git a/features/poll/api/src/main/res/values-it/translations.xml b/features/poll/api/src/main/res/values-it/translations.xml index 1a634bd625..7b24b9a008 100644 --- a/features/poll/api/src/main/res/values-it/translations.xml +++ b/features/poll/api/src/main/res/values-it/translations.xml @@ -4,5 +4,6 @@ "%1$d percento dei voti totali" "%1$d percento dei voti totali" + "Rimuoverà la selezione precedente" "Questa è la risposta vincente" diff --git a/features/poll/api/src/main/res/values-ko/translations.xml b/features/poll/api/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..e5c0c06f46 --- /dev/null +++ b/features/poll/api/src/main/res/values-ko/translations.xml @@ -0,0 +1,8 @@ + + + + "%1$d 총 투표율" + + "이전 선택 항목을 제거합니다" + "이것이 승리의 답입니다" + 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 index 88681aca30..77bbb3d7f5 100644 --- a/features/poll/api/src/main/res/values-pt-rBR/translations.xml +++ b/features/poll/api/src/main/res/values-pt-rBR/translations.xml @@ -4,5 +4,6 @@ "%1$d por cento de todos os votos" "%1$d por cento de todos os votos" + "Removerá a seleção anterior" "Esta é a resposta vencedora" diff --git a/features/poll/api/src/main/res/values-ro/translations.xml b/features/poll/api/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..ef86b4ff16 --- /dev/null +++ b/features/poll/api/src/main/res/values-ro/translations.xml @@ -0,0 +1,10 @@ + + + + "%1$d la suta din totalul voturilor" + "%1$d la suta din totalul voturilor" + "%1$d la suta din totalul voturilor" + + "Va șterge selecția anterioară" + "Acesta este votul câștigător" + diff --git a/features/poll/api/src/main/res/values-ru/translations.xml b/features/poll/api/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..0e97fc77f0 --- /dev/null +++ b/features/poll/api/src/main/res/values-ru/translations.xml @@ -0,0 +1,10 @@ + + + + "%1$d процент от общего числа голосов" + "%1$d процента от общего числа голосов" + "%1$d процентов от общего числа голосов" + + "Удалить предыдущий ответ" + "Это лучший ответ" + diff --git a/features/poll/api/src/main/res/values-sv/translations.xml b/features/poll/api/src/main/res/values-sv/translations.xml index 54b7f62d01..e04a499624 100644 --- a/features/poll/api/src/main/res/values-sv/translations.xml +++ b/features/poll/api/src/main/res/values-sv/translations.xml @@ -4,5 +4,6 @@ "%1$d procent av totala röster" "%1$d procent av totala röster" + "Kommer att ta bort föregående val" "Detta är det vinnande svaret" diff --git a/features/poll/api/src/main/res/values-zh/translations.xml b/features/poll/api/src/main/res/values-zh/translations.xml new file mode 100644 index 0000000000..773d2b03fc --- /dev/null +++ b/features/poll/api/src/main/res/values-zh/translations.xml @@ -0,0 +1,8 @@ + + + + "%1$d 总投票百分比" + + "将移除之前的选择" + "这是获胜的答案" + diff --git a/features/poll/impl/build.gradle.kts b/features/poll/impl/build.gradle.kts index 40c44480df..99ebefa71c 100644 --- a/features/poll/impl/build.gradle.kts +++ b/features/poll/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.poll.api) @@ -35,18 +36,10 @@ dependencies { implementation(projects.libraries.dateformatter.api) 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(libs.test.robolectric) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.analytics.test) testImplementation(projects.features.messages.test) - testImplementation(projects.tests.testutils) testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.features.poll.test) - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } 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 1bb0d87405..e028209ae8 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 @@ -7,17 +7,18 @@ package io.element.android.features.poll.impl.actions -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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.timeline.Timeline import io.element.android.services.analytics.api.AnalyticsService -import javax.inject.Inject @ContributesBinding(RoomScope::class) -class DefaultEndPollAction @Inject constructor( +@Inject +class DefaultEndPollAction( private val analyticsService: AnalyticsService, ) : EndPollAction { override suspend fun execute(timeline: Timeline, pollStartId: EventId): Result { 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 757fe1803e..a067757357 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 @@ -7,17 +7,18 @@ package io.element.android.features.poll.impl.actions -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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.timeline.Timeline import io.element.android.services.analytics.api.AnalyticsService -import javax.inject.Inject @ContributesBinding(RoomScope::class) -class DefaultSendPollResponseAction @Inject constructor( +@Inject +class DefaultSendPollResponseAction( private val analyticsService: AnalyticsService, ) : SendPollResponseAction { override suspend fun execute(timeline: Timeline, pollStartId: EventId, answerId: String): Result { 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 1a397b96e6..992fde1c4e 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 @@ -13,10 +13,10 @@ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.features.poll.api.create.CreatePollMode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs @@ -26,7 +26,8 @@ import io.element.android.services.analytics.api.AnalyticsService import java.util.concurrent.atomic.AtomicBoolean @ContributesNode(RoomScope::class) -class CreatePollNode @AssistedInject constructor( +@AssistedInject +class CreatePollNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: CreatePollPresenter.Factory, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt index f9f8e59ea8..88b1ad52a0 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt @@ -16,9 +16,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.Composer import im.vector.app.features.analytics.plan.PollCreation import io.element.android.features.messages.api.MessageComposerContext @@ -37,7 +37,8 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch import timber.log.Timber -class CreatePollPresenter @AssistedInject constructor( +@AssistedInject +class CreatePollPresenter( repositoryFactory: PollRepository.Factory, private val analyticsService: AnalyticsService, private val messageComposerContext: MessageComposerContext, @@ -46,7 +47,7 @@ class CreatePollPresenter @AssistedInject constructor( @Assisted private val timelineMode: Timeline.Mode, ) : Presenter { @AssistedFactory - interface Factory { + fun interface Factory { fun create( timelineMode: Timeline.Mode, backNavigator: () -> Unit, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt index 019da10de9..3c76aad7c8 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt @@ -10,14 +10,15 @@ package io.element.android.features.poll.impl.create 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.poll.api.create.CreatePollEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultCreatePollEntryPoint @Inject constructor() : CreatePollEntryPoint { +@Inject +class DefaultCreatePollEntryPoint : CreatePollEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreatePollEntryPoint.NodeBuilder { val plugins = ArrayList() 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 ad73b0583f..ee53ca7826 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,9 @@ package io.element.android.features.poll.impl.data -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject 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 @@ -26,13 +26,14 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first -class PollRepository @AssistedInject constructor( +@AssistedInject +class PollRepository( private val room: JoinedRoom, private val defaultTimelineProvider: TimelineProvider, @Assisted private val timelineMode: Timeline.Mode, ) { @AssistedFactory - interface Factory { + fun interface Factory { fun create( timelineMode: Timeline.Mode, ): PollRepository diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/DefaultPollHistoryEntryPoint.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/DefaultPollHistoryEntryPoint.kt index 9787216a50..8c89a47c65 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/DefaultPollHistoryEntryPoint.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/DefaultPollHistoryEntryPoint.kt @@ -9,14 +9,15 @@ package io.element.android.features.poll.impl.history import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.poll.api.history.PollHistoryEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultPollHistoryEntryPoint @Inject constructor() : PollHistoryEntryPoint { +@Inject +class DefaultPollHistoryEntryPoint : PollHistoryEntryPoint { override fun createNode(parentNode: Node, buildContext: BuildContext): Node { return parentNode.createNode(buildContext) } diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryFlowNode.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryFlowNode.kt index 48a222e0e7..19142508a1 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryFlowNode.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryFlowNode.kt @@ -15,9 +15,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.poll.api.create.CreatePollEntryPoint import io.element.android.features.poll.api.create.CreatePollMode import io.element.android.libraries.architecture.BackstackView @@ -29,7 +29,8 @@ import io.element.android.libraries.matrix.api.timeline.Timeline import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) -class PollHistoryFlowNode @AssistedInject constructor( +@AssistedInject +class PollHistoryFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val createPollEntryPoint: CreatePollEntryPoint, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryNode.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryNode.kt index f75b268dd6..3fdfdb921f 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryNode.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryNode.kt @@ -13,14 +13,15 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId @ContributesNode(RoomScope::class) -class PollHistoryNode @AssistedInject constructor( +@AssistedInject +class PollHistoryNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: PollHistoryPresenter, 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 b144f31609..15bc803f51 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 @@ -17,6 +17,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.SendPollResponseAction import io.element.android.features.poll.impl.history.model.PollHistoryFilter @@ -29,9 +30,9 @@ import io.element.android.libraries.matrix.api.timeline.Timeline import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject -class PollHistoryPresenter @Inject constructor( +@Inject +class PollHistoryPresenter( @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val sendPollResponseAction: SendPollResponseAction, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt index 114cd9b20d..2a9c802e6b 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt @@ -7,6 +7,7 @@ package io.element.android.features.poll.impl.history.model +import dev.zacsweers.metro.Inject import io.element.android.features.poll.api.pollcontent.PollContentStateFactory import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.dateformatter.api.DateFormatter @@ -15,9 +16,9 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.withContext -import javax.inject.Inject -class PollHistoryItemsFactory @Inject constructor( +@Inject +class PollHistoryItemsFactory( private val pollContentStateFactory: PollContentStateFactory, private val dateFormatter: DateFormatter, private val dispatchers: CoroutineDispatchers, @@ -44,7 +45,12 @@ class PollHistoryItemsFactory @Inject constructor( return when (timelineItem) { is MatrixTimelineItem.Event -> { val pollContent = timelineItem.event.content as? PollContent ?: return null - val pollContentState = pollContentStateFactory.create(timelineItem.event, pollContent) + val pollContentState = pollContentStateFactory.create( + eventId = timelineItem.eventId, + isEditable = timelineItem.event.isEditable, + isOwn = timelineItem.event.isOwn, + content = pollContent, + ) PollHistoryItem( formattedDate = dateFormatter.format( timestamp = timelineItem.event.timestamp, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/model/DefaultPollContentStateFactory.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/model/DefaultPollContentStateFactory.kt index 7ab0d33bc1..8647d80f23 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/model/DefaultPollContentStateFactory.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/model/DefaultPollContentStateFactory.kt @@ -7,25 +7,28 @@ package io.element.android.features.poll.impl.model -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.poll.api.pollcontent.PollAnswerItem import io.element.android.features.poll.api.pollcontent.PollContentState import io.element.android.features.poll.api.pollcontent.PollContentStateFactory import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.isDisclosed -import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import kotlinx.collections.immutable.toImmutableList -import javax.inject.Inject @ContributesBinding(RoomScope::class) -class DefaultPollContentStateFactory @Inject constructor( +@Inject +class DefaultPollContentStateFactory( private val matrixClient: MatrixClient, ) : PollContentStateFactory { override suspend fun create( - event: EventTimelineItem, - content: PollContent + eventId: EventId?, + isEditable: Boolean, + isOwn: Boolean, + content: PollContent, ): PollContentState { val totalVoteCount = content.votes.flatMap { it.value }.size val myVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys @@ -58,13 +61,13 @@ class DefaultPollContentStateFactory @Inject constructor( } return PollContentState( - eventId = event.eventId, + eventId = eventId, question = content.question, answerItems = answerItems.toImmutableList(), pollKind = content.kind, - isPollEditable = event.isEditable, + isPollEditable = isEditable, isPollEnded = isPollEnded, - isMine = event.isOwn, + isMine = isOwn, ) } } 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 a7c2939d42..dc19f4db5c 100644 --- a/features/poll/impl/src/main/res/values-de/translations.xml +++ b/features/poll/impl/src/main/res/values-de/translations.xml @@ -4,17 +4,17 @@ "Ergebnisse erst nach Ende der Umfrage anzeigen" "Anonyme Umfrage" "Option %1$d" - "Ihre Änderungen wurden nicht gespeichert. Sind Sie sicher, dass Sie zurückgehen wollen?" + "Deine Änderungen wurden nicht gespeichert. Bist du sicher, dass du zurückgehen willst?" "Lösche Option %1$s" "Frage oder Thema" "Worum geht es bei der Umfrage?" "Umfrage erstellen" - "Möchten Sie diese Umfrage wirklich löschen?" + "Möchtest du diese Umfrage wirklich löschen?" "Umfrage löschen" "Umfrage bearbeiten" "Keine laufenden Umfragen vorhanden." "Keine beendeten Umfragen vorhanden." - "Aktuell" - "Vergangenheit" + "Laufend" + "Beendet" "Umfragen" diff --git a/features/poll/impl/src/main/res/values-ko/translations.xml b/features/poll/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..3c85a48219 --- /dev/null +++ b/features/poll/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,20 @@ + + + "옵션 추가" + "투표가 끝난 이후에만 결과 표시" + "투표 숨기기" + "옵션 %1$d" + "변경 내용이 저장되지 않았습니다. 정말로 돌아가시겠습니까?" + "삭제 옵션 %1$s" + "질문 또는 주제" + "무슨 투표인가요?" + "투표 생성" + "정말 이 투표를 삭제하시겠습니까?" + "투표 삭제" + "투표 수정" + "진행 중인 투표를 찾을 수 없습니다." + "과거의 투표를 찾을 수 없습니다." + "진행 중" + "과거" + "투표" + 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 d1eb60cc23..afbae890d5 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 @@ -3,14 +3,14 @@ "Adicionar opção" "Mostrar resultados somente após o término da enquete" "Ocultar votos" - "Opção %1$d" + "%1$dª opção" "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" - "Tem certeza de que quer deletar esta enquete?" - "Excluir Enquete" + "Tem certeza de que quer apagar esta enquete?" + "Excluir enquete" "Editar enquete" "Não foi possível encontrar nenhuma enquete em andamento." "Não foi possível encontrar nenhuma enquete anterior." diff --git a/features/poll/impl/src/main/res/values-ro/translations.xml b/features/poll/impl/src/main/res/values-ro/translations.xml index 1163453b3f..65cf16ebcb 100644 --- a/features/poll/impl/src/main/res/values-ro/translations.xml +++ b/features/poll/impl/src/main/res/values-ro/translations.xml @@ -5,6 +5,7 @@ "Sondaj anonim" "Opțiune %1$d" "Modificările dumneavoastră nu au fost salvate. Sunteți sigur că doriți să vă întoarceți?" + "Ștergeți opțiunea %1$s" "Întrebare sau subiect" "Despre ce este sondajul?" "Creați un sondaj" diff --git a/features/poll/impl/src/main/res/values-ru/translations.xml b/features/poll/impl/src/main/res/values-ru/translations.xml index dbc7a49e5e..ac4be39b09 100644 --- a/features/poll/impl/src/main/res/values-ru/translations.xml +++ b/features/poll/impl/src/main/res/values-ru/translations.xml @@ -5,6 +5,7 @@ "Скрыть голоса" "Вариант %1$d" "Изменения не сохранены. Вы действительно хотите вернуться?" + "Удалить опцию %1$s" "Вопрос или тема" "О чём будет опрос?" "Создать опрос" diff --git a/features/poll/impl/src/main/res/values-uz/translations.xml b/features/poll/impl/src/main/res/values-uz/translations.xml index ee41d67459..8ea1c4b827 100644 --- a/features/poll/impl/src/main/res/values-uz/translations.xml +++ b/features/poll/impl/src/main/res/values-uz/translations.xml @@ -4,8 +4,16 @@ "Natijalarni faqat soʻrov tugagandan keyin koʻrsatish" "Ovozlarni yashirish" "Variant%1$d" + "Oʻzgarishlar saqlanmadi. Haqiqatan ham orqaga qaytmoqchimisiz?" "Savol yoki mavzu" "So\'rovnoma nima haqida?" "So‘rovnoma yaratish" + "Siz rostdan ham bu soʻrovnomani oʻchirib tashlamoqchimisiz?" + "So‘rovnomani o‘chirish" "So‘rovnomani tahrirlash" + "Davom etayotgan soʻrovlar topilmadi." + "Avvalgi soʻrovnomalar topilmadi." + "Jarayonda" + "Oʻtgan" + "Soʻrovnomalar" diff --git a/features/poll/impl/src/main/res/values-zh/translations.xml b/features/poll/impl/src/main/res/values-zh/translations.xml index efaa01ed2f..f231e99d76 100644 --- a/features/poll/impl/src/main/res/values-zh/translations.xml +++ b/features/poll/impl/src/main/res/values-zh/translations.xml @@ -5,6 +5,7 @@ "隐藏投票" "选项 %1$d" "更改尚未保存,确定要返回吗?" + "删除选项%1$s" "问题或话题" "投票的内容是什么?" "创建投票" diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPointTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPointTest.kt new file mode 100644 index 0000000000..bf77cb4535 --- /dev/null +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPointTest.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.poll.impl.create + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.test.FakeMessageComposerContext +import io.element.android.features.poll.api.create.CreatePollEntryPoint +import io.element.android.features.poll.api.create.CreatePollMode +import io.element.android.features.poll.impl.data.PollRepository +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultCreatePollEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultCreatePollEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + CreatePollNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { timelineMode: Timeline.Mode, backNavigator: () -> Unit, mode: CreatePollMode -> + CreatePollPresenter( + repositoryFactory = { + val room = FakeJoinedRoom() + PollRepository(room, LiveTimelineProvider(room), timelineMode) + }, + analyticsService = FakeAnalyticsService(), + messageComposerContext = FakeMessageComposerContext(), + navigateUp = backNavigator, + mode = mode, + timelineMode = timelineMode, + ) + }, + analyticsService = FakeAnalyticsService(), + ) + } + val params = CreatePollEntryPoint.Params( + timelineMode = Timeline.Mode.Live, + mode = CreatePollMode.NewPoll, + ) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .build() + assertThat(result).isInstanceOf(CreatePollNode::class.java) + assertThat(result.plugins).contains(CreatePollNode.Inputs(timelineMode = params.timelineMode, mode = params.mode)) + } +} diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/DefaultPollHistoryEntryPointTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/DefaultPollHistoryEntryPointTest.kt new file mode 100644 index 0000000000..dfab33e837 --- /dev/null +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/DefaultPollHistoryEntryPointTest.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.poll.impl.history + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.poll.api.create.CreatePollEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class DefaultPollHistoryEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() = runTest { + val entryPoint = DefaultPollHistoryEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + PollHistoryFlowNode( + buildContext = buildContext, + plugins = plugins, + createPollEntryPoint = object : CreatePollEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + } + ) + } + val result = entryPoint.createNode(parentNode, BuildContext.root(null)) + assertThat(result).isInstanceOf(PollHistoryFlowNode::class.java) + } +} 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 eeb4d0d223..cac5fae135 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 @@ -151,23 +151,23 @@ class PollHistoryPresenterTest { assert(paginateLambda).isCalledExactly(2) } } - - private fun TestScope.createPollHistoryPresenter( - room: FakeJoinedRoom = FakeJoinedRoom(), - endPollAction: EndPollAction = FakeEndPollAction(), - sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), - pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory( - pollContentStateFactory = DefaultPollContentStateFactory(FakeMatrixClient()), - dateFormatter = FakeDateFormatter(), - dispatchers = testCoroutineDispatchers(), - ), - ): PollHistoryPresenter { - return PollHistoryPresenter( - sessionCoroutineScope = this, - sendPollResponseAction = sendPollResponseAction, - endPollAction = endPollAction, - pollHistoryItemFactory = pollHistoryItemFactory, - room = room, - ) - } +} + +internal fun TestScope.createPollHistoryPresenter( + room: FakeJoinedRoom = FakeJoinedRoom(), + endPollAction: EndPollAction = FakeEndPollAction(), + sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), + pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory( + pollContentStateFactory = DefaultPollContentStateFactory(FakeMatrixClient()), + dateFormatter = FakeDateFormatter(), + dispatchers = testCoroutineDispatchers(), + ), +): PollHistoryPresenter { + return PollHistoryPresenter( + sessionCoroutineScope = this, + sendPollResponseAction = sendPollResponseAction, + endPollAction = endPollAction, + pollHistoryItemFactory = pollHistoryItemFactory, + room = room, + ) } diff --git a/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/pollcontent/FakePollContentStateFactory.kt b/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/pollcontent/FakePollContentStateFactory.kt index 4c467fddd9..701d2e4640 100644 --- a/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/pollcontent/FakePollContentStateFactory.kt +++ b/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/pollcontent/FakePollContentStateFactory.kt @@ -10,20 +10,20 @@ package io.element.android.features.poll.test.pollcontent import io.element.android.features.poll.api.pollcontent.PollAnswerItem import io.element.android.features.poll.api.pollcontent.PollContentState import io.element.android.features.poll.api.pollcontent.PollContentStateFactory -import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import kotlinx.collections.immutable.toImmutableList class FakePollContentStateFactory : PollContentStateFactory { - override suspend fun create(event: EventTimelineItem, content: PollContent): PollContentState { + override suspend fun create(eventId: EventId?, isEditable: Boolean, isOwn: Boolean, content: PollContent): PollContentState { return PollContentState( - eventId = event.eventId, + eventId = eventId, question = content.question, answerItems = emptyList().toImmutableList(), pollKind = content.kind, - isPollEditable = event.isEditable, + isPollEditable = isEditable, isPollEnded = content.endTime != null, - isMine = event.isOwn + isMine = isOwn, ) } } 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 f41d497b18..c0affde2df 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 @@ -15,7 +15,6 @@ 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 { @@ -41,9 +40,10 @@ interface PreferencesEntryPoint : FeatureEntryPoint { } interface Callback : Plugin { + fun onAddAccount() fun onOpenBugReport() fun onSecureBackupClick() fun onOpenRoomNotificationSettings(roomId: RoomId) - fun navigateTo(sessionId: SessionId, roomId: RoomId, eventId: EventId) + fun navigateTo(roomId: RoomId, eventId: EventId) } } diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index a74896686d..eb057a9d53 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -1,6 +1,7 @@ import config.BuildTimeConfig import extension.buildConfigFieldStr -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -42,7 +43,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.androidutils) @@ -71,7 +72,6 @@ dependencies { implementation(projects.features.rageshake.api) implementation(projects.features.lockscreen.api) implementation(projects.features.analytics.api) - implementation(projects.features.ftue.api) implementation(projects.features.licenses.api) implementation(projects.features.logout.api) implementation(projects.features.deactivation.api) @@ -91,13 +91,7 @@ dependencies { implementation(platform(libs.network.okhttp.bom)) implementation(libs.network.okhttp) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.test.mockk) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.mediapickers.test) @@ -106,15 +100,12 @@ dependencies { testImplementation(projects.libraries.preferences.test) 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.logout.test) testImplementation(projects.libraries.indicator.test) testImplementation(projects.libraries.pushproviders.test) + testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.services.analytics.test) testImplementation(projects.services.toolbox.test) - 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/DefaultCacheService.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultCacheService.kt index d9d8ad77a4..da42cf87dd 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultCacheService.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultCacheService.kt @@ -7,18 +7,19 @@ package io.element.android.features.preferences.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.preferences.api.CacheService -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultCacheService @Inject constructor() : CacheService { +@Inject +class DefaultCacheService : CacheService { private val _clearedCacheEventFlow = MutableSharedFlow(0) override val clearedCacheEventFlow: Flow = _clearedCacheEventFlow diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt index a7234fe7f2..d0efc2107e 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt @@ -10,14 +10,15 @@ package io.element.android.features.preferences.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultPreferencesEntryPoint @Inject constructor() : PreferencesEntryPoint { +@Inject +class DefaultPreferencesEntryPoint : PreferencesEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): PreferencesEntryPoint.NodeBuilder { return object : PreferencesEntryPoint.NodeBuilder { val plugins = ArrayList() 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 b38b835f36..c9ae2862c1 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 @@ -17,9 +17,9 @@ import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.deactivation.api.AccountDeactivationEntryPoint import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint import io.element.android.features.lockscreen.api.LockScreenEntryPoint @@ -41,14 +41,14 @@ 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) -class PreferencesFlowNode @AssistedInject constructor( +@AssistedInject +class PreferencesFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val lockScreenEntryPoint: LockScreenEntryPoint, @@ -116,6 +116,10 @@ class PreferencesFlowNode @AssistedInject constructor( return when (navTarget) { NavTarget.Root -> { val callback = object : PreferencesRootNode.Callback { + override fun onAddAccount() { + plugins().forEach { it.onAddAccount() } + } + override fun onOpenBugReport() { plugins().forEach { it.onOpenBugReport() } } @@ -207,6 +211,10 @@ class PreferencesFlowNode @AssistedInject constructor( navigateUp() } } + + override fun openIgnoredUsers() { + backstack.push(NavTarget.BlockedUsers) + } }) .build() } @@ -221,8 +229,8 @@ class PreferencesFlowNode @AssistedInject constructor( } } - override fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId) { - plugins().forEach { it.navigateTo(sessionId, roomId, eventId) } + override fun navigateTo(roomId: RoomId, eventId: EventId) { + plugins().forEach { it.navigateTo(roomId, eventId) } } }) .build() @@ -260,7 +268,7 @@ class PreferencesFlowNode @AssistedInject constructor( .build() } is NavTarget.OssLicenses -> { - openSourceLicensesEntryPoint.getNode(this, buildContext) + openSourceLicensesEntryPoint.createNode(this, buildContext) } NavTarget.AccountDeactivation -> { accountDeactivationEntryPoint.createNode(this, buildContext) 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 08be506990..37de32bab7 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 @@ -14,15 +14,16 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class AboutNode @AssistedInject constructor( +@AssistedInject +class AboutNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: AboutPresenter, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutPresenter.kt index 8f0a8096d4..f36dc50543 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutPresenter.kt @@ -8,10 +8,11 @@ package io.element.android.features.preferences.impl.about import androidx.compose.runtime.Composable +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter -import javax.inject.Inject -class AboutPresenter @Inject constructor() : Presenter { +@Inject +class AboutPresenter : Presenter { @Composable override fun present(): AboutState { return AboutState( diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsNode.kt index f861b76eca..adcf12cf42 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsNode.kt @@ -12,13 +12,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class AdvancedSettingsNode @AssistedInject constructor( +@AssistedInject +class AdvancedSettingsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: AdvancedSettingsPresenter, 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 152e71901c..6378b3038d 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 @@ -13,6 +13,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import dev.zacsweers.metro.Inject import io.element.android.compound.theme.Theme import io.element.android.compound.theme.mapToTheme import io.element.android.libraries.architecture.Presenter @@ -25,9 +26,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch -import javax.inject.Inject -class AdvancedSettingsPresenter @Inject constructor( +@Inject +class AdvancedSettingsPresenter( private val appPreferencesStore: AppPreferencesStore, private val sessionPreferencesStore: SessionPreferencesStore, private val mediaPreviewConfigStateStore: MediaPreviewConfigStateStore, 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 index 49f199ec3f..b2d7243ccc 100644 --- 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 @@ -9,13 +9,14 @@ 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 dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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 @@ -27,7 +28,6 @@ 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, @@ -45,7 +45,8 @@ interface MediaPreviewConfigStateStore { @ContributesBinding(SessionScope::class) @SingleIn(SessionScope::class) -class DefaultMediaPreviewConfigStateStore @Inject constructor( +@Inject +class DefaultMediaPreviewConfigStateStore( @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val mediaPreviewService: MediaPreviewService, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsNode.kt index 87f61a9025..7f96e8f181 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsNode.kt @@ -12,13 +12,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class AnalyticsSettingsNode @AssistedInject constructor( +@AssistedInject +class AnalyticsSettingsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: AnalyticsSettingsPresenter, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenter.kt index 84060b4687..4f833b783d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenter.kt @@ -8,11 +8,12 @@ package io.element.android.features.preferences.impl.analytics import androidx.compose.runtime.Composable +import dev.zacsweers.metro.Inject import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState import io.element.android.libraries.architecture.Presenter -import javax.inject.Inject -class AnalyticsSettingsPresenter @Inject constructor( +@Inject +class AnalyticsSettingsPresenter( private val analyticsPreferencesPresenter: Presenter, ) : Presenter { @Composable diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersNode.kt index 2dda157ce9..4c94a8dfb4 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersNode.kt @@ -12,13 +12,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class BlockedUsersNode @AssistedInject constructor( +@AssistedInject +class BlockedUsersNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: BlockedUsersPresenter, 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 c673276a17..8a61bd4bed 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 @@ -16,6 +16,7 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState @@ -27,9 +28,9 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import javax.inject.Inject -class BlockedUsersPresenter @Inject constructor( +@Inject +class BlockedUsersPresenter( private val matrixClient: MatrixClient, private val featureFlagService: FeatureFlagService, ) : Presenter { 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 392a6af903..6208d0123e 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 @@ -15,14 +15,15 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.designsystem.showkase.getBrowserIntent import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class DeveloperSettingsNode @AssistedInject constructor( +@AssistedInject +class DeveloperSettingsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: DeveloperSettingsPresenter, 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 9bd3d93dac..e07c30bac9 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 @@ -19,6 +19,7 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshots.SnapshotStateMap +import dev.zacsweers.metro.Inject import io.element.android.features.preferences.impl.developer.tracing.toLogLevel import io.element.android.features.preferences.impl.developer.tracing.toLogLevelItem import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase @@ -46,9 +47,9 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.net.URL -import javax.inject.Inject -class DeveloperSettingsPresenter @Inject constructor( +@Inject +class DeveloperSettingsPresenter( private val featureFlagService: FeatureFlagService, private val computeCacheSizeUseCase: ComputeCacheSizeUseCase, private val clearCacheUseCase: ClearCacheUseCase, 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 15a6afcd89..f488889c5b 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 @@ -13,13 +13,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class NotificationSettingsNode @AssistedInject constructor( +@AssistedInject +class NotificationSettingsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: NotificationSettingsPresenter, 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 93b7f27b06..1fcb984924 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 @@ -18,6 +18,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -39,10 +40,10 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import javax.inject.Inject import kotlin.time.Duration.Companion.seconds -class NotificationSettingsPresenter @Inject constructor( +@Inject +class NotificationSettingsPresenter( private val notificationSettingsService: NotificationSettingsService, private val userPushStoreFactory: UserPushStoreFactory, private val matrixClient: MatrixClient, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/SystemNotificationsEnabledProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/SystemNotificationsEnabledProvider.kt index 309d3d4c68..8dc8b3d2bd 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/SystemNotificationsEnabledProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/SystemNotificationsEnabledProvider.kt @@ -8,10 +8,10 @@ package io.element.android.features.preferences.impl.notifications import androidx.core.app.NotificationManagerCompat -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn -import javax.inject.Inject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn interface SystemNotificationsEnabledProvider { fun notificationsEnabled(): Boolean @@ -19,7 +19,8 @@ interface SystemNotificationsEnabledProvider { @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultSystemNotificationsEnabledProvider @Inject constructor( +@Inject +class DefaultSystemNotificationsEnabledProvider( private val notificationManager: NotificationManagerCompat, ) : SystemNotificationsEnabledProvider { override fun notificationsEnabled(): Boolean { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt index a56a8d6444..ccba221d9a 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt @@ -13,16 +13,17 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId @ContributesNode(SessionScope::class) -class EditDefaultNotificationSettingNode @AssistedInject constructor( +@AssistedInject +class EditDefaultNotificationSettingNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: EditDefaultNotificationSettingPresenter.Factory diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt index 4eeb56b08a..25b062827f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt @@ -15,9 +15,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingStateNoSuccess @@ -37,7 +37,8 @@ import kotlinx.coroutines.launch import java.text.Collator import kotlin.time.Duration.Companion.seconds -class EditDefaultNotificationSettingPresenter @AssistedInject constructor( +@AssistedInject +class EditDefaultNotificationSettingPresenter( private val notificationSettingsService: NotificationSettingsService, @Assisted private val isOneToOne: Boolean, private val roomListService: RoomListService, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvents.kt index ff74cebb51..87074ec7f9 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvents.kt @@ -7,6 +7,9 @@ package io.element.android.features.preferences.impl.root +import io.element.android.libraries.matrix.api.core.SessionId + sealed interface PreferencesRootEvents { data object OnVersionInfoClick : PreferencesRootEvents + data class SwitchToSession(val sessionId: SessionId) : PreferencesRootEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt index c1c98be353..1bb322108f 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 @@ -15,9 +15,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutView @@ -26,13 +26,15 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.user.MatrixUser @ContributesNode(SessionScope::class) -class PreferencesRootNode @AssistedInject constructor( +@AssistedInject +class PreferencesRootNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: PreferencesRootPresenter, private val directLogoutView: DirectLogoutView, ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { + fun onAddAccount() fun onOpenBugReport() fun onSecureBackupClick() fun onOpenAnalytics() @@ -47,6 +49,10 @@ class PreferencesRootNode @AssistedInject constructor( fun onOpenAccountDeactivation() } + private fun onAddAccount() { + plugins().forEach { it.onAddAccount() } + } + private fun onOpenBugReport() { plugins().forEach { it.onOpenBugReport() } } @@ -118,6 +124,7 @@ class PreferencesRootNode @AssistedInject constructor( state = state, modifier = modifier, onBackClick = this::navigateUp, + onAddAccountClick = this::onAddAccount, onOpenRageShake = this::onOpenBugReport, onOpenAnalytics = this::onOpenAnalytics, onOpenAbout = this::onOpenAbout, 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 b0bcfed44c..ebb9a5a867 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 @@ -17,24 +17,33 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.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 +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.core.UserId import io.element.android.libraries.matrix.api.oidc.AccountManagementAction +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import javax.inject.Inject -class PreferencesRootPresenter @Inject constructor( +@Inject +class PreferencesRootPresenter( private val matrixClient: MatrixClient, private val sessionVerificationService: SessionVerificationService, private val analyticsService: AnalyticsService, @@ -44,6 +53,8 @@ class PreferencesRootPresenter @Inject constructor( private val directLogoutPresenter: Presenter, private val showDeveloperSettingsProvider: ShowDeveloperSettingsProvider, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, + private val featureFlagService: FeatureFlagService, + private val sessionStore: SessionStore, ) : Presenter { @Composable override fun present(): PreferencesRootState { @@ -54,6 +65,25 @@ class PreferencesRootPresenter @Inject constructor( matrixClient.getUserProfile() } + val isMultiAccountEnabled by remember { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.MultiAccount) + }.collectAsState(initial = false) + + val otherSessions by remember { + sessionStore.sessionsFlow().map { list -> + list + .filter { it.userId != matrixClient.sessionId.value } + .map { + MatrixUser( + userId = UserId(it.userId), + displayName = it.userDisplayName, + avatarUrl = it.userAvatarUrl, + ) + } + .toPersistentList() + } + }.collectAsState(initial = persistentListOf()) + val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() val hasAnalyticsProviders = remember { analyticsService.getAvailableAnalyticsProviders().isNotEmpty() } @@ -95,6 +125,9 @@ class PreferencesRootPresenter @Inject constructor( is PreferencesRootEvents.OnVersionInfoClick -> { showDeveloperSettingsProvider.unlockDeveloperSettings(coroutineScope) } + is PreferencesRootEvents.SwitchToSession -> coroutineScope.launch { + sessionStore.setLatestSession(event.sessionId.value) + } } } @@ -102,6 +135,8 @@ class PreferencesRootPresenter @Inject constructor( myUser = matrixUser.value, version = versionFormatter.get(), deviceId = matrixClient.deviceId, + isMultiAccountEnabled = isMultiAccountEnabled, + otherSessions = otherSessions, showSecureBackup = !canVerifyUserSession, showSecureBackupBadge = showSecureBackupIndicator, accountManagementUrl = accountManagementUrl.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 ebe8aaf57f..830c397c59 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 @@ -11,11 +11,14 @@ 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.core.DeviceId import io.element.android.libraries.matrix.api.user.MatrixUser +import kotlinx.collections.immutable.ImmutableList data class PreferencesRootState( val myUser: MatrixUser, val version: String, val deviceId: DeviceId?, + val isMultiAccountEnabled: Boolean, + val otherSessions: ImmutableList, val showSecureBackup: Boolean, val showSecureBackupBadge: Boolean, val accountManagementUrl: String?, 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 91b32fe12d..604cb10c4d 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 @@ -11,15 +11,20 @@ import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.toPersistentList fun aPreferencesRootState( - myUser: MatrixUser, + myUser: MatrixUser = aMatrixUser(), + otherSessions: List = emptyList(), eventSink: (PreferencesRootEvents) -> Unit = { _ -> }, ) = PreferencesRootState( myUser = myUser, version = "Version 1.1 (1)", deviceId = DeviceId("ILAKNDNASDLK"), + isMultiAccountEnabled = true, + otherSessions = otherSessions.toPersistentList(), showSecureBackup = true, showSecureBackupBadge = true, accountManagementUrl = "aUrl", 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 85c180ddac..56aa4bb126 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 @@ -8,6 +8,7 @@ package io.element.android.features.preferences.impl.root import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable @@ -23,11 +24,14 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.user.UserPreferences import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferencePage +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.ListItem @@ -38,12 +42,15 @@ import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbar import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.MatrixUserProvider +import io.element.android.libraries.matrix.ui.components.MatrixUserRow +import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.ui.strings.CommonStrings @Composable fun PreferencesRootView( state: PreferencesRootState, onBackClick: () -> Unit, + onAddAccountClick: () -> Unit, onSecureBackupClick: () -> Unit, onManageAccountClick: (url: String) -> Unit, onOpenAnalytics: () -> Unit, @@ -74,7 +81,12 @@ fun PreferencesRootView( }, user = state.myUser, ) - + if (state.isMultiAccountEnabled) { + MultiAccountSection( + state = state, + onAddAccountClick = onAddAccountClick, + ) + } // 'Manage my app' section ManageAppSection( state = state, @@ -114,6 +126,38 @@ fun PreferencesRootView( } } +@Composable +private fun ColumnScope.MultiAccountSection( + state: PreferencesRootState, + onAddAccountClick: () -> Unit, +) { + HorizontalDivider( + thickness = 8.dp, + color = ElementTheme.colors.bgSubtleSecondary, + ) + state.otherSessions.forEach { matrixUser -> + MatrixUserRow( + modifier = Modifier.clickable { + state.eventSink(PreferencesRootEvents.SwitchToSession(matrixUser.userId)) + }, + matrixUser = matrixUser, + avatarSize = AvatarSize.AccountItem, + ) + HorizontalDivider() + } + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())), + headlineContent = { + Text(stringResource(CommonStrings.common_add_another_account)) + }, + onClick = onAddAccountClick, + ) + HorizontalDivider( + thickness = 8.dp, + color = ElementTheme.colors.bgSubtleSecondary, + ) +} + @Composable private fun ColumnScope.ManageAppSection( state: PreferencesRootState, @@ -214,9 +258,6 @@ private fun ColumnScope.GeneralSection( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Settings())), onClick = onOpenAdvancedSettings, ) - if (state.showDeveloperSettings) { - DeveloperPreferencesView(onOpenDeveloperSettings) - } ListItem( headlineContent = { Text(stringResource(id = CommonStrings.action_signout)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.SignOut())), @@ -231,6 +272,10 @@ private fun ColumnScope.GeneralSection( onClick = onDeactivateClick, ) } + // Put developer settings at the end, so nothing bad happens if the user clicks 8 times to enable the entry + if (state.showDeveloperSettings) { + DeveloperPreferencesView(onOpenDeveloperSettings) + } } @Composable @@ -286,6 +331,7 @@ private fun ContentToPreview(matrixUser: MatrixUser) { PreferencesRootView( state = aPreferencesRootState(myUser = matrixUser), onBackClick = {}, + onAddAccountClick = {}, onOpenAnalytics = {}, onOpenRageShake = {}, onOpenDeveloperSettings = {}, @@ -301,3 +347,16 @@ private fun ContentToPreview(matrixUser: MatrixUser) { onDeactivateClick = {}, ) } + +@PreviewsDayNight +@Composable +internal fun MultiAccountSectionPreview() = ElementPreview { + Column { + MultiAccountSection( + state = aPreferencesRootState( + otherSessions = aMatrixUserList(), + ), + onAddAccountClick = {}, + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/VersionFormatter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/VersionFormatter.kt index 63c4681d22..ce65f62f37 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/VersionFormatter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/VersionFormatter.kt @@ -7,19 +7,20 @@ package io.element.android.features.preferences.impl.root -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.di.AppScope import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.toolbox.api.strings.StringProvider -import javax.inject.Inject interface VersionFormatter { fun get(): String } @ContributesBinding(AppScope::class) -class DefaultVersionFormatter @Inject constructor( +@Inject +class DefaultVersionFormatter( private val stringProvider: StringProvider, private val buildMeta: BuildMeta, ) : VersionFormatter { 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 9b90c8ba53..50d9cf8798 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 @@ -9,33 +9,32 @@ package io.element.android.features.preferences.impl.tasks import android.content.Context import coil3.SingletonImageLoader -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.ftue.api.state.FtueService +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.Provider import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.preferences.impl.DefaultCacheService import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.annotations.ApplicationContext 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 -import javax.inject.Provider interface ClearCacheUseCase { suspend operator fun invoke() } @ContributesBinding(SessionScope::class) -class DefaultClearCacheUseCase @Inject constructor( +@Inject +class DefaultClearCacheUseCase( @ApplicationContext private val context: Context, private val matrixClient: MatrixClient, private val coroutineDispatchers: CoroutineDispatchers, private val defaultCacheService: DefaultCacheService, private val okHttpClient: Provider, - private val ftueService: FtueService, private val pushService: PushService, private val seenInvitesStore: SeenInvitesStore, private val activeRoomsHolder: ActiveRoomsHolder, @@ -51,11 +50,10 @@ class DefaultClearCacheUseCase @Inject constructor( it.memoryCache?.clear() } // Clear OkHttp cache - okHttpClient.get().cache?.delete() + okHttpClient().cache?.delete() // Clear app cache context.cacheDir.deleteRecursively() // Clear some settings - ftueService.reset() seenInvitesStore.clear() // Ensure any error will be displayed again pushService.setIgnoreRegistrationError(matrixClient.sessionId, false) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ComputeCacheSizeUseCase.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ComputeCacheSizeUseCase.kt index 46e565689b..10b1748590 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ComputeCacheSizeUseCase.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ComputeCacheSizeUseCase.kt @@ -8,22 +8,23 @@ package io.element.android.features.preferences.impl.tasks import android.content.Context -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.file.getSizeOfFiles import io.element.android.libraries.androidutils.filesize.FileSizeFormatter 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.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClient import kotlinx.coroutines.withContext -import javax.inject.Inject interface ComputeCacheSizeUseCase { suspend operator fun invoke(): String } @ContributesBinding(SessionScope::class) -class DefaultComputeCacheSizeUseCase @Inject constructor( +@Inject +class DefaultComputeCacheSizeUseCase( @ApplicationContext private val context: Context, private val matrixClient: MatrixClient, private val coroutineDispatchers: CoroutineDispatchers, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt index 059ac44a83..d691508427 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt @@ -12,16 +12,17 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.user.MatrixUser @ContributesNode(SessionScope::class) -class EditUserProfileNode @AssistedInject constructor( +@AssistedInject +class EditUserProfileNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: EditUserProfilePresenter.Factory, 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 fc8aa0175c..0cd0144986 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 @@ -19,9 +19,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.core.net.toUri -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.androidutils.file.TemporaryUriDeleter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter @@ -41,7 +41,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber -class EditUserProfilePresenter @AssistedInject constructor( +@AssistedInject +class EditUserProfilePresenter( @Assisted private val matrixUser: MatrixUser, private val matrixClient: MatrixClient, private val mediaPickerProvider: PickerProvider, 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 2ed16d6582..d87b205d5e 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 @@ -7,15 +7,16 @@ package io.element.android.features.preferences.impl.utils +import dev.zacsweers.metro.Inject 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 -class ShowDeveloperSettingsProvider @Inject constructor( +@Inject +class ShowDeveloperSettingsProvider( buildMeta: BuildMeta, ) { companion object { 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 289c59d360..bfcab248c6 100644 --- a/features/preferences/impl/src/main/res/values-bg/translations.xml +++ b/features/preferences/impl/src/main/res/values-bg/translations.xml @@ -1,15 +1,25 @@ "Изберете как да получавате известия" + "Режим за програмисти" + "Активирайте, за да имате достъп до функции и функционалности за програмисти." "Скриване на профилните снимки в заявките за покана за стая" + "Качвайте снимки и видеоклипове по-бързо и намалете използването на данни" "Оптимизиране на качеството на медията" "Модерация и безопасност" + "Изключете редактора за форматиран текст, за да пишете Markdown ръчно." "Потвърждения за прочитане" + "Ако е изключено, вашите потвърждения за прочитане няма да бъдат изпращани на никого. Все още ще получавате потвърждения за прочитане от други потребители." "Споделяне на присъствието" + "Ако е изключено, няма да можете да изпращате или получавате потвърждения за прочитане или известия за писане." "Скриване винаги" "Показване винаги" "В частни стаи" + "Скрита мултимедия винаги може да бъде показана, като се докосне" + "Показване на мултимедия в хронологията" + "Активиране на опцията за преглед на изходния код на съобщението в хронологията." "Отблокиране" + "Ще можете да виждате отново всички съобщения от тях." "Отблокиране на потребителя" "Име" "Вашето Име" @@ -18,13 +28,17 @@ "Редактиране на профила" "Обновяване на профила…" "Допълнителни настройки" + "Аудио и видео разговори" + "Несъответствие в конфигурацията" "Директни чатове" "Персонализирана настройка за чат" + "Възникна грешка при обновяването на настройките за известия." "Всички съобщения" "Само споменавания и ключови думи" "В директни чатове да бъда известяван за" "В групови чатове да бъда известяван за" "Включване на известията на това устройство" + "Конфигурацията не е оправена, моля, опитайте отново." "Групови чатове" "Покани" "Вашият сървър не поддържа тази опция в шифровани стаи, може да не получавате известия в някои стаи." @@ -34,5 +48,8 @@ "Известяване за @room" "За да получавате известия, моля, променете своя %1$s" "системни настройки" + "Системните известия са изключени" "Известия" + "Отстраняване на неизправности" + "Отстраняване на неизправности с известията" 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 db7f28a99d..9fe8715cc3 100644 --- a/features/preferences/impl/src/main/res/values-cs/translations.xml +++ b/features/preferences/impl/src/main/res/values-cs/translations.xml @@ -13,6 +13,13 @@ "Rychlejší nahrávání fotografií a videí a snížení spotřeby dat" "Optimalizace kvality médií" "Moderování a bezpečnost" + "Automaticky optimalizovat obrázky pro rychlejší nahrávání a menší velikosti souborů." + "Optimalizace kvality nahrávání obrázků" + "%1$s. Klepnutím sem provedete změnu." + "Vysoká (1080p)" + "Nízká (480p)" + "Standardní (720p)" + "Kvalita nahrávání videa" "Poskytovatel push oznámení" "Vypněte editor formátovaného textu pro ruční zadání Markdown." "Potvrzení o přečtení" diff --git a/features/preferences/impl/src/main/res/values-cy/translations.xml b/features/preferences/impl/src/main/res/values-cy/translations.xml index da0308b003..9711eda179 100644 --- a/features/preferences/impl/src/main/res/values-cy/translations.xml +++ b/features/preferences/impl/src/main/res/values-cy/translations.xml @@ -13,6 +13,13 @@ "Llwythwch i fyny lluniau a fideos yn gynt a lleihau\'r defnydd o ddata" "Optimeiddio ansawdd y cyfryngau" "Cymedroli a Diogelwch" + "Optimeiddio delweddau\'n awtomatig ar gyfer llwytho cyflymach a meintiau ffeiliau llai." + "Optimeiddio ansawdd llwytho delweddau" + "%1$s Tapiwch yma i newid." + "Uchel (1080p)" + "Isel (480c)" + "Safonol (720p)" + "Ansawdd lwytho fideo" "Darparwr hysbysiad gwthio" "Analluogi\'r golygydd testun cyfoethog i deipio Markdown â llaw." "Derbynebau darllen" diff --git a/features/preferences/impl/src/main/res/values-da/translations.xml b/features/preferences/impl/src/main/res/values-da/translations.xml index 4a29222be7..acf9fe4087 100644 --- a/features/preferences/impl/src/main/res/values-da/translations.xml +++ b/features/preferences/impl/src/main/res/values-da/translations.xml @@ -8,7 +8,7 @@ "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 avatarer i anmodninger om invitation til rum" "Skjul forhåndsvisning af medier i tidslinjen" "Upload fotos og videoer hurtigere, og reducér dataforbrug" "Optimér mediekvaliteten" @@ -65,7 +65,7 @@ Hvis du fortsætter, kan nogle af dine indstillinger blive ændret." "Alle" "Omtaler" "Giv mig besked om" - "Giv mig besked på @room" + "Giv mig besked ved @room" "For at modtage notifikationer, skal du ændre din %1$s ." "systemindstillinger" "Systemmeddelelser slået fra" 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 eb25ba482d..4072207fb4 100644 --- a/features/preferences/impl/src/main/res/values-de/translations.xml +++ b/features/preferences/impl/src/main/res/values-de/translations.xml @@ -1,31 +1,38 @@ - "Damit Sie keine wichtigen Anrufe verpassen, ändern Sie bitte Ihre Einstellungen, so dass das gesperrte Telefon auch Benachrichtigungen im Vollbildmodus erhalten darf." + "Damit du keinen wichtigen Anruf verpasst, ändere bitte deine Einstellungen so, dass du bei gesperrtem Telefon Benachrichtigungen im Vollbildmodus erhältst." "Verbessere dein Anruferlebnis" - "Wählen Sie, wie Sie Benachrichtigungen erhalten möchten" + "Wähle aus, wie du Benachrichtigungen erhalten möchtest" "Entwicklermodus" "Aktivieren, um Zugriff auf Features und Funktionen für Entwickler zu aktivieren." "Benutzerdefinierte Element Call Basis-URL" "Lege eine eigene Basis-URL für Element Call fest." - "Ungültige URL, bitte geben Sie das Protokoll (http/https) und die richtige Adresse an." + "Ungültige URL, bitte gib 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" + "Lade Fotos und Videos schneller hoch und reduziere den Datenverbrauch" + "Optimiere die Medienqualität" "Moderation und Sicherheit" - "Anbieter für Push-Benachrichtigungen" + "Optimiere Bilder automatisch für schnellere Uploads und kleinere Dateigrößen." + "Optimiere die Qualität zum Hochladen von Bildern." + "%1$s. Tippe hier, um zu ändern." + "Hoch (1080p)" + "Niedrig (480p)" + "Standard (720p)" + "Video-Upload-Qualität" + "Dienst 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. Sie erhalten weiterhin Lesebestätigungen von anderen Benutzern." + "Wenn diese Option deaktiviert ist, werden deine Lesebestätigungen an niemanden gesendet. Du erhältst weiterhin Lesebestätigungen von anderen Nutzern." "Präsenz teilen" - "Wenn diese Option deaktiviert ist, können Sie keine Lesebestätigungen oder Tippbenachrichtigungen senden oder empfangen." - "Immer verstecken" + "Wenn diese Option deaktiviert ist, kannst du keine Lesebestätigungen oder Tipp-Indikatoren senden oder empfangen." + "Immer ausblenden" "Immer anzeigen" - "In privaten Chatrooms" + "In privaten Chats" "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" + "Aktiviere die Option, um die Quelle der Nachricht im Nachrichtenverlauf zu sehen." + "Du hast keine blockierten Nutzer" "Blockierung aufheben" "Der Nutzer kann dir wieder Nachrichten senden & alle Nachrichten des Nutzers werden wieder angezeigt." "Blockierung aufheben" @@ -53,7 +60,7 @@ Wenn du fortfährst, können sich einige deiner Einstellungen ändern." "Die Konfiguration wurde nicht korrigiert, bitte versuche es erneut." "Gruppenchats" "Einladungen" - "Ihr Homeserver unterstützt diese Option in verschlüsselten Räumen nicht. In einigen Räumen werden Sie möglicherweise nicht benachrichtigt." + "Dein Homeserver unterstützt diese Option in verschlüsselten Chats nicht. In einigen Chats erhältst du möglicherweise keine Benachrichtigungen." "Erwähnungen" "Alle" "Erwähnungen" @@ -65,5 +72,5 @@ Wenn du fortfährst, können sich einige deiner Einstellungen ändern." "Benachrichtigungen" "Verlauf pushen" "Fehlerbehebung" - "Beheben Sie die Fehler bei Benachrichtigungen" + "Fehlerbehebung für Benachrichtigungen" 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 f938235e66..1240b620e9 100644 --- a/features/preferences/impl/src/main/res/values-fi/translations.xml +++ b/features/preferences/impl/src/main/res/values-fi/translations.xml @@ -13,6 +13,13 @@ "Lähetä valokuvia ja videoita nopeammin ja vähennä datan käyttöä." "Optimoi median laatu" "Moderointi ja Turvallisuus" + "Optimoi kuvat automaattisesti nopeampia lähetysnopeuksia ja pienempiä tiedostokokoja varten." + "Optimoi kuvien lähetyslaatu" + "%1$s. Napauta tästä vaihtaaksesi." + "Korkea (1080p)" + "Matala (480p)" + "Normaali (720p)" + "Videon lähetyslaatu" "Push-ilmoitusten tarjoaja" "Ota rikastettu tekstieditori pois käytöstä, jotta voit kirjoittaa Markdownia manuaalisesti." "Lukukuittaukset" 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 d64864549d..c998c1646a 100644 --- a/features/preferences/impl/src/main/res/values-hu/translations.xml +++ b/features/preferences/impl/src/main/res/values-hu/translations.xml @@ -34,8 +34,8 @@ "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" - "Újra láthatja az összes üzenetét." - "Felhasználó kitiltásának feloldása" + "Újra látni fogja az összes üzenetét." + "Felhasználó letiltásának feloldása" "Tiltás feloldása…" "Megjelenítendő név" "Saját megjelenítendő név" @@ -70,7 +70,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" + "Leküldéses értesítések előzményei" "Hibaelhárítás" "Értesítések hibaelhárítása" 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 677a4f01c3..0a37b0d394 100644 --- a/features/preferences/impl/src/main/res/values-it/translations.xml +++ b/features/preferences/impl/src/main/res/values-it/translations.xml @@ -13,12 +13,19 @@ "Carica foto e video più velocemente e riduci l\'utilizzo dei dati" "Ottimizza la qualità dei contenuti multimediali" "Moderazione e Sicurezza" + "Ottimizza automaticamente le immagini per caricamenti più rapidi e file di dimensioni ridotte." + "Ottimizza la qualità del caricamento delle immagini" + "%1$s. Tocca qui per cambiarla." + "Alta (1080p)" + "Bassa (480p)" + "Standard (720p)" + "Qualità del caricamento video" "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." + "Conferme di visualizzazione" + "Se disattivato, le tue conferme di visualizzazione non verranno inviate a nessuno. Riceverai comunque conferme di visualizzazione da altri utenti." "Condividi presenza online" - "Se disattivato, non potrai inviare o ricevere ricevute di lettura o notifiche di scrittura." + "Se disattivato, non potrai né inviare né ricevere conferme di lettura o notifiche di scrittura." "Nascondi sempre" "Mostra sempre" "Nelle stanze private" diff --git a/features/preferences/impl/src/main/res/values-ko/translations.xml b/features/preferences/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..065384af32 --- /dev/null +++ b/features/preferences/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,77 @@ + + + "중요한 전화를 놓치지 않으려면 휴대폰이 잠겨 있을 때 전체 화면 알림을 허용하도록 설정을 변경하세요." + "통화 경험을 향상시키세요" + "어떻게 알림을 받을지 선택하기" + "개발자 모드" + "개발자가 기능에 액세스할 수 있도록 합니다." + "사용자 정의 요소 호출 베이스 URL" + "Element Call에 대한 사용자 지정 기본 URL을 설정하세요." + "URL이 잘못되었습니다. 프로토콜(http/https)과 올바른 주소를 포함했는지 확인하세요." + "방 초대 요청에서 아바타 숨기기" + "타임라인에서 미디어 미리 보기 숨기기" + "사진과 동영상을 더 빠르게 업로드하고 데이터 사용량을 줄이세요" + "미디어 품질 최적화" + "중재와 안전" + "더 빠른 업로드와 더 작은 파일 크기에 맞춰 이미지를 자동으로 최적화합니다." + "이미지 업로드 품질 최적화" + "%1$s. 여기를 탭하여 변경하세요." + "고화질 (1080p)" + "저화질 (480p)" + "표준 화질 (720p) +" + "비디오 업로드 품질" + "푸시 알림 제공자" + "마크다운을 직접 입력하려면 서식 있는 텍스트 편집기를 비활성화하세요." + "읽기 확인" + "이 기능을 해제하면 읽기 확인이 누구에게도 전송되지 않습니다. 다른 사용자의 읽기 확인은 계속 수신됩니다." + "현재 상태 공유" + "이 기능을 해제하면 읽기 확인 및 타이핑 알림을 보내거나 받을 수 없습니다." + "항상 숨기기" + "항상 표시" + "비공개 방에서" + "숨겨진 미디어는 터치로 표시할 수 있습니다." + "타임라인에 미디어 표시" + "타임라인에서 메시지 소스를 볼 수 있는 옵션을 활성화합니다." + "차단된 사용자가 없습니다." + "차단 해제" + "그들로부터 보낸 모든 메시지를 다시 볼 수 있게 됩니다." + "사용자 차단 해제" + "차단 해제 중…" + "표시되는 이름" + "내 표시되는 이름" + "알 수 없는 오류가 발생하여 정보를 변경할 수 없습니다." + "프로필을 업데이트할 수 없음" + "프로필 수정" + "프로필 업데이트 중…" + "추가 설정" + "음성 및 동영상 통화" + "구성 불일치" + "알림 설정을 간소화하여 옵션을 더 쉽게 찾을 수 있도록 했습니다. 과거에 선택한 일부 맞춤 설정은 여기에서 표시되지 않지만, 여전히 활성화되어 있습니다. + +계속 진행하면 일부 설정이 변경될 수 있습니다." + "직접 채팅" + "채팅별 맞춤 설정" + "알림 설정 업데이트 중 오류가 발생했습니다." + "모든 메시지" + "언급 및 키워드만" + "다이렉트 채팅에서 알림 받기" + "그룹 채팅에서 나에게 알림을 보내세요" + "이 장치에서 알림 사용" + "설정이 수정되지 않았습니다. 다시 시도해 주세요." + "그룹 채팅" + "초대" + "귀하의 홈서버는 암호화된 방에서 이 옵션을 지원하지 않으므로, 일부 방에서는 알림이 표시되지 않을 수 있습니다." + "언급" + "모두" + "언급" + "나에게 알려주세요" + @room 에서 알림 받기 + "알림을 받으려면 %1$s 을 변경해 주세요." + "시스템 설정" + "시스템 알림이 꺼져 있습니다." + "알림" + "푸시 기록" + "문제 해결" + "문제 해결 알림" + 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 a6a1396dec..5b3e75e7ec 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 @@ -4,16 +4,16 @@ "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" + "Ative para ter acesso a recursos e funcionalidades para desenvolvedores." + "URL base do Element Call personalizada" + "Defina uma URL base personalizada para o Element Call." + "URL inválida, por favor verifique se o protocolo (http/https) está incluso e o endereço correto." + "Ocultar avatares em solicitações de convite para salas" + "Ocultar pré-visualizações de mídia na linha do tempo" + "Envie fotos e vídeos com mais rapidez e reduza o uso de dados" + "Otimizar a qualidade da mídia" "Moderação e segurança" - "Provedor de notificações por push" + "Provedor de notificações 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." @@ -24,43 +24,43 @@ "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." + "Ative a opção para visualizar o fonte da mensagem na linha do tempo." "Você não tem usuários bloqueados" "Desbloquear" - "Você poderá ver todas as mensagens deles novamente." + "Você poderá ver todas as mensagens desta pessoa novamente." "Desbloquear usuário" "Desbloqueando…" "Nome de exibição" "Seu nome de exibição" - "Um erro desconhecido foi encontrado e as informações não puderam ser alteradas." + "Ocorreu um erro desconhecido e as informações não puderam ser alteradas." "Não foi possível atualizar o perfil" "Editar perfil" "Atualizando o perfil…" "Configurações adicionais" "Chamadas de áudio e vídeo" - "Incompatibilidade de configuração" + "Não correspondência de configuração" "Simplificamos as configurações de notificações para facilitar a localização das opções. Algumas configurações personalizadas que você escolheu no passado não são mostradas aqui, mas ainda estão ativas. Se você continuar, algumas de suas configurações poderão mudar." - "Conversas privadas" - "Configuração personalizada por chat" + "Conversas diretas" + "Configuração personalizada por conversa" "Ocorreu um erro ao atualizar a configuração de notificação." "Todas as mensagens" "Somente menções e palavras-chave" - "Em conversas privadas, me notifique para" - "Em conversas em grupos, me notifique para" + "Em conversas diretas, me notifique de" + "Em conversas em grupos, me notifique de" "Ativar notificações neste dispositivo" "A configuração não foi corrigida, tente novamente." - "Bate-papos em grupo" + "Conversas 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." + "Seu servidor-casa não suporta esta opção em salas criptografadas. Você pode não ser notificado em algumas salas." "Menções" "Todos" "Menções" - "Me notifique para" - "Notifique-me em @room" - "Para receber notificações, altere seu %1$s." - "configurações do sistema" + "Me notifique de" + "Notifique-me quando usam o @room" + "Para receber notificações, altere as %1$s." + "configurações do seu sistema" "Notificações do sistema desativadas" "Notificações" "Histórico de push" 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 af380cf9ac..f80947d2f7 100644 --- a/features/preferences/impl/src/main/res/values-pt/translations.xml +++ b/features/preferences/impl/src/main/res/values-pt/translations.xml @@ -13,6 +13,13 @@ "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" + "Otimiza automaticamente as imagens para carregamentos mais rápidos e tamanhos de ficheiros mais pequenos." + "Optimiza a qualidade do carregamento de imagens" + "%1$s. Toca aqui para alterar." + "Alta (1080p)" + "Baixa (480p)" + "Padrão (720p)" + "Qualidade de carregamento do vídeo" "Fornecedor de envio" "Desativa o editor de texto rico para poderes escrever Markdown manualmente." "Recibos de leitura" diff --git a/features/preferences/impl/src/main/res/values-ro/translations.xml b/features/preferences/impl/src/main/res/values-ro/translations.xml index bf0e2b2f9c..5977771ee6 100644 --- a/features/preferences/impl/src/main/res/values-ro/translations.xml +++ b/features/preferences/impl/src/main/res/values-ro/translations.xml @@ -8,12 +8,29 @@ "Adresa URL de bază Element Call" "Setați o adresă URL de bază personalizată pentru Element Call." "URL invalid, vă rugăm să vă asigurați că includeți protocolul (http/https) și adresa corectă." + "Ascundeți avatarele din invitațiile pentru camere" + "Ascundeți previzualizările media în lista de mesaje" + "Încărcați fotografii și videoclipuri mai rapid și reduceți consumul de date" + "Optimizați calitatea media" + "Moderare și siguranță" + "Optimizați automat imaginile pentru încărcări mai rapide și dimensiuni mai mici ale fișierelor." + "Optimizați calitatea încărcării imaginilor" + "%1$s. Atingeți aici pentru a schimba." + "Înaltă (1080p)" + "Scăzută (480p)" + "Standard (720p)" + "Calitatea încărcării videoclipurilor" "Furnizor de notificări push" "Dezactivați editorul avansat pentru a tasta manual Markdown." "Chitanțe de citire" "Dacă dezactivată, chitanțele dumneavoastră de citire nu vor fi trimise nimănui. Veți primi în continuare chitanțe de citire de la alți utilizatori." "Împărtășiți prezența" "Dacă dezactivată, nu veți putea trimite sau primi chitanțe de citire sau notificări de tastare." + "Ascundeţi întotdeauna" + "Afișați întotdeauna" + "În camere private" + "Un fișier media ascuns poate fi afișat oricând prin apăsarea pe acesta." + "Afișați conținutul media în lista de mesaje" "Activați opțiunea pentru a vizualiza sursa mesajelor." "Nu aveți utilizatori blocați" "Deblocați" @@ -55,6 +72,7 @@ Dacă continuați, unele dintre setările dumneavoastră pot fi modificate.""Setări de sistem" "Notificările de sistem sunt dezactivate" "Notificări" + "Istoricul notificărilor" "Depanare" "Depanați notificările" diff --git a/features/preferences/impl/src/main/res/values-ru/translations.xml b/features/preferences/impl/src/main/res/values-ru/translations.xml index 70b15002a6..59d19b0210 100644 --- a/features/preferences/impl/src/main/res/values-ru/translations.xml +++ b/features/preferences/impl/src/main/res/values-ru/translations.xml @@ -13,6 +13,10 @@ "Загружайте фотографии и видео быстрее и сокращайте потребление трафика" "Оптимизировать качество мультимедиа" "Модерация и безопасность" + "Высокое (1080p)" + "Низкое (480p)" + "Среднее (720p)" + "Качество загружаемого видео" "Поставщик push-уведомлений" "Отключить редактор форматированного текста и включить Markdown." "Уведомления о прочтении" 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 831280255c..e25e36a44f 100644 --- a/features/preferences/impl/src/main/res/values-sv/translations.xml +++ b/features/preferences/impl/src/main/res/values-sv/translations.xml @@ -13,6 +13,13 @@ "Ladda upp foton och videor snabbare och minska dataanvändningen" "Optimera mediekvaliteten" "Moderering och säkerhet" + "Optimera bilder automatiskt för snabbare uppladdningar och mindre filstorlekar." + "Optimera bilduppladdningskvalitet" + "%1$s. Tryck här för att ändra." + "Hög (1080p)" + "Låg (480p)" + "Standard (720p)" + "Videouppladdningskvalitet" "Pushnotisleverantör" "Inaktivera rik-text-redigeraren för att skriva Markdown manuellt." "Läskvitton" diff --git a/features/preferences/impl/src/main/res/values-tr/translations.xml b/features/preferences/impl/src/main/res/values-tr/translations.xml index a13a006e22..fd0a94955c 100644 --- a/features/preferences/impl/src/main/res/values-tr/translations.xml +++ b/features/preferences/impl/src/main/res/values-tr/translations.xml @@ -8,8 +8,11 @@ "Ö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." + "Oda davet isteklerinde avatarları gizle" + "Zaman çizelgesinde medya ön izlemelerini kapat" "Fotoğraf ve videoları daha hızlı yükleyin ve veri kullanımını azaltın" "Medya kalitesini optimize edin" + "Yönetim ve Güvenlik" "Anlık bildirim sağlayıcısı" "Markdown\'ı manuel olarak yazmak için zengin metin düzenleyicisini devre dışı bırakın." "Okundu bilgisi" 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 ba246e61f3..63a4db26f1 100644 --- a/features/preferences/impl/src/main/res/values-uk/translations.xml +++ b/features/preferences/impl/src/main/res/values-uk/translations.xml @@ -13,6 +13,9 @@ "Швидше завантажуйте фотографії та відео та зменшуйте використання даних" "Оптимізуйте медіаякість" "Модерування й безпека" + "Автоматична оптимізація зображень для швидшого вивантаження та зменшення розміру файлів." + "Оптимізація якості вивантажуваних зображень" + "%1$s, торкніться тут, щоб змінити." "Висока (1080p)" "Низька (480p)" "Стандартна (720p)" diff --git a/features/preferences/impl/src/main/res/values-uz/translations.xml b/features/preferences/impl/src/main/res/values-uz/translations.xml index d8b27d0b5c..b01bd11fbd 100644 --- a/features/preferences/impl/src/main/res/values-uz/translations.xml +++ b/features/preferences/impl/src/main/res/values-uz/translations.xml @@ -1,15 +1,27 @@ + "Muhim qoʻngʻiroqlarni oʻtkazib yubormasligingiz uchun telefoningiz qulflangan holatida toʻliq ekranli bildirishnomalarni ko‘rsatishga ruxsat beradigan qilib sozlamalaringizni oʻzgartiring." + "Qoʻngʻiroq tajribangizni yaxshilang" "Bildirishnomalarni qanday qabul qilishni tanlang" "Dasturchi rejimi" "Ishlab chiquvchilar uchun xususiyatlar va funksiyalarga kirishni yoqing." "Maxsus element qo‘ng‘iroqlar bazasi URL manzili" "Element qo\'ng\'irog\'iga maxsus asosiy url or\'natish" "URL noto‘g‘ri, iltimos, protokol (http/https) va to‘g‘ri manzilni kiritganingizga ishonch hosil qiling." + "Rasm va videolarni tezroq yuklang va trafik sarfini kamaytiring" + "Media sifatini yaxshilash" + "Push bildirishnoma provayderi" "Boy matn muharriri o\'chiring Markdown bilan qo\'lda yozish uchun" + "Kvitansiyalarni oʻqish" + "Agar oʻchirib qo‘yilsa, sizning oʻqilganlik bildirishnomangiz hech kimga yuborilmaydi. Siz boshqa foydalanuvchilardan oʻqilganlik bildirishnomalarini olishda davom etasiz." + "Mavjudligini ulashish" + "Agar oʻchirib qoʻyilsa, siz oʻqilganlik haqidagi bildirishnomalarni yoki yozayotganingiz haqidagi xabarlarni yubora olmaysiz va qabul qila olmaysiz." + "Xabar manbasini vaqt jadvalida ko‘rish imkoniyatini yoqing." + "Sizda bloklangan foydalanuvchi yo‘q" "Blokdan chiqarish" "Ulardan kelgan barcha xabarlarni yana koʻrishingiz mumkin boʻladi." "Foydalanuvchini blokdan chiqarish" + "Blokdan chiqarilmoqda…" "Ko\'rsatiladigan ism" "Ismingizni ko\'rsating" "Noma\'lum xatolik yuz berdi va ma\'lumotni o\'zgartirib bo\'lmadi." @@ -32,6 +44,8 @@ Davom ettirsangiz, baʼzi sozlamalaringiz oʻzgarishi mumkin." "Ushbu qurilmada bildirishnomalarni yoqing" "Konfiguratsiya tuzatilmadi, qayta urinib ko\'ring." "Guruh suhbatlari" + "Taklifnomalar" + "Uy serveringiz shifrlangan xonalarda ushbu imkoniyatni qoʻllab-quvvatlamaydi, shuning uchun baʼzi xonalardagi xabarlarni olmasligingiz mumkin." "Eslatmalar" "Hammasi" "Eslatmalar" @@ -41,4 +55,6 @@ Davom ettirsangiz, baʼzi sozlamalaringiz oʻzgarishi mumkin." "tizim sozlamalari" "Tizim bildirishnomalari o\'chirilgan" "Bildirishnomalar" + "Muammolarni bartaraf etish" + "Bildirishnomalar bilan bog‘liq muammolarni bartaraf etish" 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 52630e1c7b..bda2b085a5 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 @@ -13,6 +13,13 @@ "上傳照片與影片更快且減少資料使用量" "最佳化媒體品質" "管理與安全" + "自動最佳化影像以提供更快的上傳速度與較小的檔案大小。" + "最佳化影像上傳品質" + "%1$s。輕點此處以變更。" + "高 (1080p)" + "低 (480p)" + "標準 (720p)" + "視訊上傳品質" "推播通知提供者" "手動輸入 Markdown,停用格式化文字編輯器。" "已讀回條" diff --git a/features/preferences/impl/src/main/res/values-zh/translations.xml b/features/preferences/impl/src/main/res/values-zh/translations.xml index 668202a193..d9f5bb3c88 100644 --- a/features/preferences/impl/src/main/res/values-zh/translations.xml +++ b/features/preferences/impl/src/main/res/values-zh/translations.xml @@ -12,6 +12,14 @@ "在时间轴中隐藏媒体预览" "针对上传进行优化" "媒体" + "内容审核与安全" + "自动优化图像以实现更快的上传速度和更小的文件大小。" + "优化图片上传质量" + "%1$s。点击此处更改。" + "高 (1080p)" + "低画质 (480p)" + "标准 (720p)" + "视频上传质量" "通知推送提供者" "禁用富文本编辑器,手动输入 Markdown。" "已读回执" diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPointTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPointTest.kt new file mode 100644 index 0000000000..9e1bd70376 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPointTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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 + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.deactivation.api.AccountDeactivationEntryPoint +import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint +import io.element.android.features.lockscreen.api.LockScreenEntryPoint +import io.element.android.features.logout.api.LogoutEntryPoint +import io.element.android.features.preferences.api.PreferencesEntryPoint +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint +import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultPreferencesEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultPreferencesEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + PreferencesFlowNode( + buildContext = buildContext, + plugins = plugins, + lockScreenEntryPoint = object : LockScreenEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: LockScreenEntryPoint.Target) = lambdaError() + override fun pinUnlockIntent(context: Context) = lambdaError() + }, + notificationTroubleShootEntryPoint = object : NotificationTroubleShootEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + pushHistoryEntryPoint = object : PushHistoryEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + logoutEntryPoint = object : LogoutEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + openSourceLicensesEntryPoint = object : OpenSourceLicensesEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + accountDeactivationEntryPoint = object : AccountDeactivationEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + ) + } + val callback = object : PreferencesEntryPoint.Callback { + override fun onAddAccount() = lambdaError() + override fun onOpenBugReport() = lambdaError() + override fun onSecureBackupClick() = lambdaError() + override fun onOpenRoomNotificationSettings(roomId: RoomId) = lambdaError() + override fun navigateTo(roomId: RoomId, eventId: EventId) = lambdaError() + } + val params = PreferencesEntryPoint.Params( + initialElement = PreferencesEntryPoint.InitialTarget.NotificationSettings, + ) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .callback(callback) + .build() + assertThat(result).isInstanceOf(PreferencesFlowNode::class.java) + assertThat(result.plugins).contains(params) + assertThat(result.plugins).contains(callback) + } + + @Test + fun `test initial target to nav target mapping`() { + assertThat(PreferencesEntryPoint.InitialTarget.Root.toNavTarget()) + .isEqualTo(PreferencesFlowNode.NavTarget.Root) + assertThat(PreferencesEntryPoint.InitialTarget.NotificationSettings.toNavTarget()) + .isEqualTo(PreferencesFlowNode.NavTarget.NotificationSettings) + assertThat(PreferencesEntryPoint.InitialTarget.NotificationTroubleshoot.toNavTarget()) + .isEqualTo(PreferencesFlowNode.NavTarget.TroubleshootNotifications) + } +} 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 42fee711a7..0f6eec3c6d 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 @@ -16,15 +16,23 @@ import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsP 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.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.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.A_USER_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.verification.FakeSessionVerificationService +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -61,6 +69,8 @@ class PreferencesRootPresenterTest { ) ) assertThat(initialState.version).isEqualTo("A Version") + assertThat(initialState.isMultiAccountEnabled).isFalse() + assertThat(initialState.otherSessions).isEmpty() val loadedState = awaitItem() assertThat(loadedState.myUser).isEqualTo( MatrixUser( @@ -174,6 +184,34 @@ class PreferencesRootPresenterTest { } } + @Test + fun `present - multiple accounts`() = runTest { + createPresenter( + matrixClient = FakeMatrixClient( + sessionId = A_SESSION_ID, + canDeactivateAccountResult = { true }, + ), + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.MultiAccount.key to true) + ), + sessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData(sessionId = A_SESSION_ID.value), + aSessionData( + sessionId = A_SESSION_ID_2.value, + userDisplayName = "Bob", + userAvatarUrl = "avatarUrl", + ), + ) + ) + ).test { + val state = awaitFirstItem() + assertThat(state.isMultiAccountEnabled).isTrue() + assertThat(state.otherSessions).hasSize(1) + assertThat(state.otherSessions[0]).isEqualTo(MatrixUser(userId = A_SESSION_ID_2, displayName = "Bob", avatarUrl = "avatarUrl")) + } + } + private suspend fun ReceiveTurbine.awaitFirstItem(): T { skipItems(1) return awaitItem() @@ -185,6 +223,8 @@ class PreferencesRootPresenterTest { showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)), rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(true) }, indicatorService: IndicatorService = FakeIndicatorService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), + sessionStore: SessionStore = InMemorySessionStore(), ) = PreferencesRootPresenter( matrixClient = matrixClient, sessionVerificationService = sessionVerificationService, @@ -195,5 +235,7 @@ class PreferencesRootPresenterTest { directLogoutPresenter = { aDirectLogoutState() }, showDeveloperSettingsProvider = showDeveloperSettingsProvider, rageshakeFeatureAvailability = rageshakeFeatureAvailability, + featureFlagService = featureFlagService, + sessionStore = sessionStore, ) } 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 cfdc63984c..1e16509ad2 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 @@ -10,7 +10,6 @@ package io.element.android.features.preferences.impl.tasks 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 @@ -41,10 +40,6 @@ class DefaultClearCacheUseCaseTest { clearCacheLambda = clearCacheLambda, ) val defaultCacheService = DefaultCacheService() - val resetFtueLambda = lambdaRecorder { } - val ftueService = FakeFtueService( - resetLambda = resetFtueLambda, - ) val setIgnoreRegistrationErrorLambda = lambdaRecorder { _, _ -> } val resetBatteryOptimizationStateResult = lambdaRecorder { } val pushService = FakePushService( @@ -59,7 +54,6 @@ class DefaultClearCacheUseCaseTest { coroutineDispatchers = testCoroutineDispatchers(), defaultCacheService = defaultCacheService, okHttpClient = { OkHttpClient.Builder().build() }, - ftueService = ftueService, pushService = pushService, seenInvitesStore = seenInvitesStore, activeRoomsHolder = activeRoomsHolder, @@ -67,7 +61,6 @@ class DefaultClearCacheUseCaseTest { defaultCacheService.clearedCacheEventFlow.test { sut.invoke() clearCacheLambda.assertions().isCalledOnce() - resetFtueLambda.assertions().isCalledOnce() setIgnoreRegistrationErrorLambda.assertions().isCalledOnce() .with(value(matrixClient.sessionId), value(false)) resetBatteryOptimizationStateResult.assertions().isCalledOnce() diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/bugreport/BugReportEntryPoint.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/bugreport/BugReportEntryPoint.kt index fbc7eb0dc8..0eb84b529b 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/bugreport/BugReportEntryPoint.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/bugreport/BugReportEntryPoint.kt @@ -21,7 +21,6 @@ interface BugReportEntryPoint : FeatureEntryPoint { } interface Callback : Plugin { - fun onBugReportSent() - fun onViewLogs(basePath: String) + fun onDone() } } diff --git a/features/rageshake/api/src/main/res/values-bg/translations.xml b/features/rageshake/api/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..c2a8c06adf --- /dev/null +++ b/features/rageshake/api/src/main/res/values-bg/translations.xml @@ -0,0 +1,4 @@ + + + "%1$s се срина при последното използване. Искате ли да споделите доклад за срива с нас?" + 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 dbee250bfa..cd8fc2de0f 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ö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?" + "%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 Fehlerberichte öffnen?" "Rageshake" "Erkennungsschwelle" diff --git a/features/rageshake/api/src/main/res/values-ko/translations.xml b/features/rageshake/api/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..846350fc65 --- /dev/null +++ b/features/rageshake/api/src/main/res/values-ko/translations.xml @@ -0,0 +1,7 @@ + + + "%1$s이(가) 이전에 마지막으로 사용할 때 충돌했습니다. 충돌 보고서를 공유해주실 수 있나요?" + "휴대폰을 강하게 흔드셨습니다. 버그 보고 화면을 여시겠어요?" + "강하게 흔들기" + "감지 수준" + diff --git a/features/rageshake/api/src/main/res/values-pt-rBR/translations.xml b/features/rageshake/api/src/main/res/values-pt-rBR/translations.xml index 3985149a27..c25eb58585 100644 --- a/features/rageshake/api/src/main/res/values-pt-rBR/translations.xml +++ b/features/rageshake/api/src/main/res/values-pt-rBR/translations.xml @@ -1,7 +1,7 @@ - "%1$s fechou inesperadamente na última vez que foi usado. Gostaria de compartilhar um relatório de falhas conosco?" - "Você parece estar sacudindo o telefone em sinal de frustração. Você gostaria de abrir a tela de relatório de erros?" - "Rageshake" - "Limiar de deteção" + "%1$s falhou inesperadamente na última vez que foi usado. Gostaria de compartilhar um relatório de falhas conosco?" + "Você parece estar sacudindo o telefone com frustração. Você gostaria de abrir a tela de relatório de bugs?" + "Agitar agressivamente" + "Fronteira de detecção" diff --git a/features/rageshake/impl/build.gradle.kts b/features/rageshake/impl/build.gradle.kts index addfe3d794..b17d78f3aa 100644 --- a/features/rageshake/impl/build.gradle.kts +++ b/features/rageshake/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -22,17 +23,19 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.appconfig) implementation(projects.features.enterprise.api) + implementation(projects.features.viewfolder.api) implementation(projects.services.toolbox.api) implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.network) implementation(projects.libraries.architecture) implementation(projects.libraries.designsystem) + implementation(projects.libraries.preferences.api) implementation(projects.libraries.uiStrings) implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.matrix.api) @@ -44,19 +47,12 @@ dependencies { implementation(libs.coil) implementation(libs.coil.compose) - testImplementation(libs.test.junit) - testImplementation(libs.test.robolectric) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.test.mockk) + testCommonDependencies(libs) testImplementation(projects.features.enterprise.test) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.sessionStorage.implMemory) testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.features.rageshake.test) - testImplementation(projects.tests.testutils) + testImplementation(projects.libraries.preferences.test) testImplementation(projects.services.toolbox.test) testImplementation(libs.network.mockwebserver) } 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 index c9f78a23ce..8cb210159b 100644 --- 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 @@ -7,16 +7,17 @@ package io.element.android.features.rageshake.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.rageshake.impl.reporter.BugReporterUrlProvider -import io.element.android.libraries.di.AppScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultRageshakeFeatureAvailability @Inject constructor( +@Inject +class DefaultRageshakeFeatureAvailability( private val bugReporterUrlProvider: BugReporterUrlProvider, ) : RageshakeFeatureAvailability { override fun isAvailable(): Flow { diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportFlowNode.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportFlowNode.kt new file mode 100644 index 0000000000..10af89f740 --- /dev/null +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportFlowNode.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.bugreport + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.pop +import com.bumble.appyx.navmodel.backstack.operation.push +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint +import io.element.android.features.viewfolder.api.ViewFolderEntryPoint +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.createNode +import kotlinx.parcelize.Parcelize + +@ContributesNode(AppScope::class) +@AssistedInject +class BugReportFlowNode( + @Assisted val buildContext: BuildContext, + @Assisted plugins: List, + private val viewFolderEntryPoint: ViewFolderEntryPoint, +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +) { + private fun onDone() { + plugins().forEach { it.onDone() } + } + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Root : NavTarget + + @Parcelize + data class ViewLogs( + val rootPath: String, + ) : NavTarget + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Root -> { + val callback = object : BugReportNode.Callback { + override fun onDone() { + this@BugReportFlowNode.onDone() + } + + override fun onViewLogs(basePath: String) { + backstack.push(NavTarget.ViewLogs(rootPath = basePath)) + } + } + createNode(buildContext, listOf(callback)) + } + is NavTarget.ViewLogs -> { + val callback = object : ViewFolderEntryPoint.Callback { + override fun onDone() { + backstack.pop() + } + } + val params = ViewFolderEntryPoint.Params( + rootPath = navTarget.rootPath, + ) + viewFolderEntryPoint + .nodeBuilder(this, buildContext) + .params(params) + .callback(callback) + .build() + } + } + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView() + } +} 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 c124971efb..e307dba8ec 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 @@ -14,24 +14,33 @@ 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.rageshake.api.bugreport.BugReportEntryPoint +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.libraries.androidutils.system.toast -import io.element.android.libraries.di.AppScope import io.element.android.libraries.ui.strings.CommonStrings @ContributesNode(AppScope::class) -class BugReportNode @AssistedInject constructor( +@AssistedInject +class BugReportNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: BugReportPresenter, private val bugReporter: BugReporter, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onDone() + fun onViewLogs(basePath: String) + } + private fun onViewLogs(basePath: String) { - plugins().forEach { it.onViewLogs(basePath) } + plugins().forEach { it.onViewLogs(basePath) } + } + + private fun onDone() { + plugins().forEach { it.onDone() } } @Composable @@ -53,8 +62,4 @@ class BugReportNode @AssistedInject constructor( } ) } - - private fun onDone() { - plugins().forEach { it.onBugReportSent() } - } } 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 f42d252080..4faef73589 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,6 +16,7 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import dev.zacsweers.metro.Inject import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.reporter.BugReporterListener import io.element.android.features.rageshake.impl.crash.CrashDataStore @@ -25,9 +26,9 @@ 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 -class BugReportPresenter @Inject constructor( +@Inject +class BugReportPresenter( private val bugReporter: BugReporter, private val crashDataStore: CrashDataStore, private val screenshotHolder: ScreenshotHolder, diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/DefaultBugReportEntryPoint.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/DefaultBugReportEntryPoint.kt index 67fec56134..6fa5772c17 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/DefaultBugReportEntryPoint.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/DefaultBugReportEntryPoint.kt @@ -10,14 +10,15 @@ package io.element.android.features.rageshake.impl.bugreport 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultBugReportEntryPoint @Inject constructor() : BugReportEntryPoint { +@Inject +class DefaultBugReportEntryPoint : BugReportEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): BugReportEntryPoint.NodeBuilder { val plugins = ArrayList() @@ -28,7 +29,7 @@ class DefaultBugReportEntryPoint @Inject constructor() : BugReportEntryPoint { } override fun build(): Node { - return parentNode.createNode(buildContext, plugins) + return parentNode.createNode(buildContext, plugins) } } } 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 7136393238..4d9f596d1d 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 @@ -14,22 +14,23 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultCrashDetectionPresenter @Inject constructor( +@Inject +class DefaultCrashDetectionPresenter( private val buildMeta: BuildMeta, private val crashDataStore: CrashDataStore, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, 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 0b86bb4ef0..a2be13b4cd 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 @@ -7,32 +7,27 @@ package io.element.android.features.rageshake.impl.crash -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking -import javax.inject.Inject - -private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_crash") private val appHasCrashedKey = booleanPreferencesKey("appHasCrashed") private val crashDataKey = stringPreferencesKey("crashData") @ContributesBinding(AppScope::class) -class PreferencesCrashDataStore @Inject constructor( - @ApplicationContext context: Context +@Inject +class PreferencesCrashDataStore( + preferenceDataStoreFactory: PreferenceDataStoreFactory, ) : CrashDataStore { - private val store = context.dataStore + private val store = preferenceDataStoreFactory.create("elementx_crash") override fun setCrashData(crashData: String) { // Must block diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt index ca310cedad..e41583b61c 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt @@ -7,7 +7,6 @@ package io.element.android.features.rageshake.impl.crash -import android.content.Context import android.os.Build import io.element.android.libraries.core.data.tryOrNull import timber.log.Timber @@ -15,9 +14,8 @@ import java.io.PrintWriter import java.io.StringWriter class VectorUncaughtExceptionHandler( - context: Context + private val preferencesCrashDataStore: PreferencesCrashDataStore, ) : Thread.UncaughtExceptionHandler { - private val crashDataStore = PreferencesCrashDataStore(context) private var previousHandler: Thread.UncaughtExceptionHandler? = null /** @@ -65,7 +63,7 @@ class VectorUncaughtExceptionHandler( append(sw.buffer.toString()) } Timber.e("FATAL EXCEPTION $bugDescription") - crashDataStore.setCrashData(bugDescription) + preferencesCrashDataStore.setCrashData(bugDescription) // Show the classical system popup previousHandler?.uncaughtException(thread, throwable) } 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 1a8aed7051..c0120bfc3a 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 @@ -14,7 +14,9 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.rageshake.api.detection.RageshakeDetectionEvents import io.element.android.features.rageshake.api.detection.RageshakeDetectionPresenter import io.element.android.features.rageshake.api.detection.RageshakeDetectionState @@ -23,14 +25,13 @@ import io.element.android.features.rageshake.api.preferences.RageshakePreference import io.element.android.features.rageshake.api.screenshot.ImageResult 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 import timber.log.Timber -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultRageshakeDetectionPresenter @Inject constructor( +@Inject +class DefaultRageshakeDetectionPresenter( private val screenshotHolder: ScreenshotHolder, private val rageShake: RageShake, private val preferencesPresenter: RageshakePreferencesPresenter, diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeBindings.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeBindings.kt new file mode 100644 index 0000000000..e1172c31fc --- /dev/null +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeBindings.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.rageshake.impl.di + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesTo +import io.element.android.features.rageshake.impl.crash.PreferencesCrashDataStore + +@ContributesTo(AppScope::class) +interface RageshakeBindings { + fun preferencesCrashDataStore(): PreferencesCrashDataStore +} diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeModule.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeModule.kt index 0df785beb0..97f4d39ec3 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeModule.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeModule.kt @@ -7,9 +7,10 @@ package io.element.android.features.rageshake.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter import io.element.android.features.rageshake.api.crash.CrashDetectionState import io.element.android.features.rageshake.api.detection.RageshakeDetectionPresenter @@ -17,10 +18,9 @@ import io.element.android.features.rageshake.api.detection.RageshakeDetectionSta import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.di.AppScope @ContributesTo(AppScope::class) -@Module +@BindingContainer interface RageshakeModule { @Binds fun bindRageshakePreferencesPresenter(presenter: RageshakePreferencesPresenter): Presenter diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/DefaultLogFilesRemover.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/DefaultLogFilesRemover.kt index 997dfae323..1122042d7f 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/DefaultLogFilesRemover.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/DefaultLogFilesRemover.kt @@ -7,15 +7,16 @@ package io.element.android.features.rageshake.impl.logs -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.rageshake.api.logs.LogFilesRemover import io.element.android.features.rageshake.impl.reporter.DefaultBugReporter -import io.element.android.libraries.di.AppScope import java.io.File -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultLogFilesRemover @Inject constructor( +@Inject +class DefaultLogFilesRemover( private val bugReporter: DefaultBugReporter, ) : LogFilesRemover { override suspend fun perform(predicate: (File) -> Boolean) { 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 6096388e21..d1072f360c 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 @@ -15,20 +15,21 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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.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 -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultRageshakePreferencesPresenter @Inject constructor( +@Inject +class DefaultRageshakePreferencesPresenter( private val rageshake: RageShake, private val rageshakeDataStore: RageshakeDataStore, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, 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 651b71c079..634e3ed65a 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 @@ -11,16 +11,18 @@ import android.content.Context import android.hardware.Sensor import android.hardware.SensorManager import androidx.core.content.getSystemService -import com.squareup.anvil.annotations.ContributesBinding import com.squareup.seismic.ShakeDetector -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.di.SingleIn -import javax.inject.Inject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metro.binding +import io.element.android.libraries.di.annotations.ApplicationContext @SingleIn(AppScope::class) -@ContributesBinding(scope = AppScope::class, boundType = RageShake::class) -class DefaultRageShake @Inject constructor( +@ContributesBinding(scope = AppScope::class, binding = binding()) +@Inject +class DefaultRageShake( @ApplicationContext context: Context, ) : ShakeDetector.Listener, RageShake { private var sensorManager = context.getSystemService() 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 9d7171b8a0..32be2f7e95 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 @@ -7,31 +7,26 @@ package io.element.android.features.rageshake.impl.rageshake -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.floatPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject - -private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_rageshake") private val enabledKey = booleanPreferencesKey("enabled") private val sensitivityKey = floatPreferencesKey("sensitivity") @ContributesBinding(AppScope::class) -class PreferencesRageshakeDataStore @Inject constructor( - @ApplicationContext context: Context +@Inject +class PreferencesRageshakeDataStore( + preferenceDataStoreFactory: PreferenceDataStoreFactory, ) : RageshakeDataStore { - private val store = context.dataStore + private val store = preferenceDataStoreFactory.create("elementx_rageshake") override fun isEnabled(): Flow { return store.data.map { prefs -> diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReportAppNameProvider.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReportAppNameProvider.kt index b63dfcf669..9e450d5edb 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReportAppNameProvider.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReportAppNameProvider.kt @@ -7,16 +7,17 @@ package io.element.android.features.rageshake.impl.reporter -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.appconfig.RageshakeConfig -import io.element.android.libraries.di.AppScope -import javax.inject.Inject fun interface BugReportAppNameProvider { fun provide(): String } @ContributesBinding(AppScope::class) -class DefaultBugReportAppNameProvider @Inject constructor() : BugReportAppNameProvider { +@Inject +class DefaultBugReportAppNameProvider : BugReportAppNameProvider { override fun provide(): String = RageshakeConfig.BUG_REPORT_APP_NAME } 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 d56c75f464..01b6857c0b 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 @@ -11,7 +11,11 @@ import android.content.Context import android.os.Build import androidx.core.net.toFile import androidx.core.net.toUri -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.Provider +import dev.zacsweers.metro.SingleIn import io.element.android.appconfig.RageshakeConfig import io.element.android.features.rageshake.api.logs.createWriteToFilesConfiguration import io.element.android.features.rageshake.api.reporter.BugReporter @@ -24,9 +28,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.mimetype.MimeTypes -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.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.SdkMetadata import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService @@ -55,15 +57,14 @@ import java.time.LocalDateTime import java.time.ZoneOffset import java.time.format.DateTimeFormatter import java.util.Locale -import javax.inject.Inject -import javax.inject.Provider /** * BugReporter creates and sends the bug reports. */ @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultBugReporter @Inject constructor( +@Inject +class DefaultBugReporter( @ApplicationContext private val context: Context, private val screenshotHolder: ScreenshotHolder, private val crashDataStore: CrashDataStore, @@ -255,7 +256,7 @@ class DefaultBugReporter @Inject constructor( var errorMessage: String? = null // trigger the request try { - response = okHttpClient.get() + response = okHttpClient() .newCall(request) .execute() } catch (e: CancellationException) { diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProvider.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProvider.kt index 3ebe5c1c29..84f8e2ca0a 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProvider.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProvider.kt @@ -7,20 +7,21 @@ package io.element.android.features.rageshake.impl.reporter -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.appconfig.RageshakeConfig import io.element.android.features.enterprise.api.BugReportUrl import io.element.android.features.enterprise.api.EnterpriseService -import io.element.android.libraries.di.AppScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultBugReporterUrlProvider @Inject constructor( +@Inject +class DefaultBugReporterUrlProvider( private val bugReportAppNameProvider: BugReportAppNameProvider, private val enterpriseService: EnterpriseService, ) : BugReporterUrlProvider { 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 dd3674ab26..5166f3d672 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 @@ -10,18 +10,19 @@ package io.element.android.features.rageshake.impl.screenshot import android.content.Context import android.graphics.Bitmap import androidx.core.net.toUri -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.libraries.androidutils.bitmap.writeBitmap import io.element.android.libraries.androidutils.file.safeDelete -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.ApplicationContext import java.io.File -import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultScreenshotHolder @Inject constructor( +@Inject +class DefaultScreenshotHolder( @ApplicationContext private val context: Context, ) : ScreenshotHolder { private val file = File(context.filesDir, "screenshot.png") diff --git a/features/rageshake/impl/src/main/res/values-bg/translations.xml b/features/rageshake/impl/src/main/res/values-bg/translations.xml index 50f792de00..7db9b494a4 100644 --- a/features/rageshake/impl/src/main/res/values-bg/translations.xml +++ b/features/rageshake/impl/src/main/res/values-bg/translations.xml @@ -11,5 +11,7 @@ "Изпращане на дневниците за сривове" "Разрешаване на дневниците" "Изпращане на екранна снимка" + "Дневниците ще бъдат включени към вашето съобщение, за да се уверим, че всичко работи правилно. За да изпратите съобщението си без дневници, изключете тази настройка." + "%1$s се срина при последното използване. Искате ли да споделите доклад за срива с нас?" "Преглед на дневниците" diff --git a/features/rageshake/impl/src/main/res/values-cs/translations.xml b/features/rageshake/impl/src/main/res/values-cs/translations.xml index 0ae9e585b1..93db5427d5 100644 --- a/features/rageshake/impl/src/main/res/values-cs/translations.xml +++ b/features/rageshake/impl/src/main/res/values-cs/translations.xml @@ -14,5 +14,7 @@ "Odeslat snímek obrazovky" "Protokoly budou součástí vaší zprávy, aby se zajistilo že vše funguje správně. Chcete-li odeslat zprávu bez protokolů, vypněte toto nastavení." "%1$s havaroval při posledním použití. Chcete se s námi podělit o zprávu o selhání?" + "Pokud máte problémy s oznámeními, nahrání nastavení oznámení nám může pomoci určit jejich příčinu." + "Nastavení odesílání oznámení" "Zobrazit protokoly" diff --git a/features/rageshake/impl/src/main/res/values-da/translations.xml b/features/rageshake/impl/src/main/res/values-da/translations.xml index 47ee436e8b..035d28162f 100644 --- a/features/rageshake/impl/src/main/res/values-da/translations.xml +++ b/features/rageshake/impl/src/main/res/values-da/translations.xml @@ -14,5 +14,7 @@ "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?" + "Hvis du har problemer med notifikationer, kan upload af notifikationsindstillingerne hjælpe os med at identificere den grundlæggende årsag." + "Send notifikationsindstillinger" "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 ac26cd075c..33baf29d59 100644 --- a/features/rageshake/impl/src/main/res/values-de/translations.xml +++ b/features/rageshake/impl/src/main/res/values-de/translations.xml @@ -1,18 +1,20 @@ "Bildschirmfoto anhängen" - "Sie können mich kontaktieren, wenn Sie weitere Fragen haben." - "Kontaktieren Sie mich" + "Du kannst mich kontaktieren, solltest du weitere Fragen haben." + "Kontaktiere mich" "Bildschirmfoto bearbeiten" - "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." + "Bitte beschreibe das Problem. Was hast du getan? Was hast du erwartet, was passiert? Was ist tatsächlich passiert? Bitte gehe so detailliert wie möglich vor." "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." + "Die Beschreibung ist zu kurz. Bitte gib weitere Informationen darüber an, was passiert ist." "Absturzprotokolle senden" "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." + "Deine Logs sind zu groß und können dem Bericht nicht beigefügt 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öchten Sie einen Absturzbericht mit uns teilen?" + "%1$s ist bei der letzten Nutzung abgestürzt. Möchtest du einen Absturzbericht mit uns teilen?" + "Wenn du Probleme mit Benachrichtigungen hast, kann das Hochladen der Einstellungen für Benachrichtigungen uns helfen, die Ursache zu finden." + "Einstellungen für Benachrichtigungen senden" "Logs ansehen" 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 64e0f9b307..2f55a9b6b5 100644 --- a/features/rageshake/impl/src/main/res/values-et/translations.xml +++ b/features/rageshake/impl/src/main/res/values-et/translations.xml @@ -14,5 +14,7 @@ "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?" + "Kui sul teavitused ei toimi päris korralikult, siis teavituste seadistuste üleslaadimine võib aidata meil põhjuse tuvastada." + "Teavituste seadistuste saatmine" "Vaata logisid" 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 0d67e42db0..6685903146 100644 --- a/features/rageshake/impl/src/main/res/values-fr/translations.xml +++ b/features/rageshake/impl/src/main/res/values-fr/translations.xml @@ -14,5 +14,7 @@ "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 ?" + "Si vous rencontrez des problèmes avec les notifications, l’envoie des paramètres de notification peut nous aider à identifier la cause du problème." + "Envoyer les paramètres de notification" "Afficher les journaux" 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 f6fcbaa1c9..851d3f0067 100644 --- a/features/rageshake/impl/src/main/res/values-hu/translations.xml +++ b/features/rageshake/impl/src/main/res/values-hu/translations.xml @@ -2,9 +2,9 @@ "Képernyőkép mellékelése" "Felveheti velem a kapcsolatot, ha bármilyen további kérdése van." - "Kapcsolat" + "Kapcsolatfelvétel" "Képernyőkép szerkesztése" - "Írja le a hibát. Mit csinált? Mire számított, hogy mi fog történni? Mi történt valójában? Fogalmazzon a lehető legrészletesebben." + "Írja le a hibát. Mit csinált? Mire számított, hogy történni fog? Mi történt valójában? Fogalmazzon a lehető legrészletesebben." "Írja le a problémát…" "Ha lehetséges, a leírást angolul írja meg." "A leírás túl rövid, adjon meg további részleteket a történtekről. Köszönjük!" @@ -14,5 +14,7 @@ "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?" + "Ha problémákat tapasztal az értesítésekkel, az értesítési beállítások feltöltése segíthet meghatároznunk a kiváltó okát." + "Értesítési beállítások küldése" "Naplók megtekintése" diff --git a/features/rageshake/impl/src/main/res/values-ko/translations.xml b/features/rageshake/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..c3d2b6204d --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,18 @@ + + + "스크린샷 첨부" + "후속 질문이 있는 경우 저에게 연락하실 수 있습니다." + "문의하기" + "스크린샷 수정" + "문제를 설명해 주세요. 무엇을 했나요? 무슨 일이 일어날 것으로 예상했나요? 실제로 무슨 일이 일어났나요. 가능한 한 자세히 설명해 주세요." + "문제를 설명해 주세요…" + "가능하다면 영어로 설명을 작성해 주십시오." + "설명 내용이 너무 짧습니다. 발생한 상황에 대해 더 자세한 내용을 제공해 주시기 바랍니다. 감사합니다!" + "충돌 로그 보내기" + "로그 허용" + "귀하의 로그가 너무 커서 이 보고서에 포함할 수 없습니다. 다른 방법으로 보내주시기 바랍니다." + "스크린샷 전송" + "모든 기능이 제대로 작동하는지 확인하기 위해 로그애 메시지가 포함됩니다. 로그 없이 메시지를 보내려면 이 설정을 해제하세요." + "%1$s이(가) 이전에 마지막으로 사용할 때 충돌했습니다. 충돌 보고서를 공유해주실 수 있나요?" + "로그 보기" + 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 c679bbd1c7..046c1b30d7 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 @@ -13,6 +13,6 @@ "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?" + "%1$s falhou inesperadamente na última vez que foi usado. Gostaria de compartilhar um relatório de falhas conosco?" "Ver registros" 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 d2e637db56..a5b44ea109 100644 --- a/features/rageshake/impl/src/main/res/values-pt/translations.xml +++ b/features/rageshake/impl/src/main/res/values-pt/translations.xml @@ -14,5 +14,7 @@ "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?" + "Se estiveres a ter problemas com as notificações, enviar as configurações pode ajudar-nos a identificar a causa." + "Enviar configurações de notificação" "Ver registos" diff --git a/features/rageshake/impl/src/main/res/values-ro/translations.xml b/features/rageshake/impl/src/main/res/values-ro/translations.xml index 4a5cd6649d..6c2a7cb611 100644 --- a/features/rageshake/impl/src/main/res/values-ro/translations.xml +++ b/features/rageshake/impl/src/main/res/values-ro/translations.xml @@ -10,6 +10,7 @@ "Descrierea este prea scurtă, vă rugăm să oferiți mai multe detalii despre ceea ce s-a întâmplat. Vă mulțumim!" "Trimiteți log-uri" "Permiteți log-uri" + "Jurnalele dumneavoastră sunt prea mari și nu pot fi incluse în acest raport. Vă rugăm să ni le trimiteți prin altă metodă." "Trimiteți captură de ecran" "Pentru a verifica că lucrurile funcționează conform așteptărilor, log-uri vor fi trimise împreună cu mesajul. Acestea vor fi private. Pentru a trimite doar mesajul, dezactivați această setare." "%1$s s-a blocat ultima dată când a fost folosit. Doriți să ne trimiteți un raport?" diff --git a/features/rageshake/impl/src/main/res/values-ru/translations.xml b/features/rageshake/impl/src/main/res/values-ru/translations.xml index 062547ee00..5a8f2ceb71 100644 --- a/features/rageshake/impl/src/main/res/values-ru/translations.xml +++ b/features/rageshake/impl/src/main/res/values-ru/translations.xml @@ -10,6 +10,7 @@ "Описание слишком короткое, пожалуйста, расскажите подробнее о том, что произошло. Спасибо!" "Отправка журналов сбоев" "Разрешить ведение журналов" + "Ваши журналы слишком большие для включения в этот отчет. Пожалуйста, отправьте их нам другим способом." "Отправить снимок экрана" "Чтобы убедиться, что все работает правильно, в сообщение будут включены журналы. Чтобы отправить сообщение без журналов, отключите эту настройку." "При последнем использовании %1$s произошел сбой. Хотите поделиться отчетом о сбое?" diff --git a/features/rageshake/impl/src/main/res/values-uz/translations.xml b/features/rageshake/impl/src/main/res/values-uz/translations.xml index a1abfd64a0..fbeb0d3271 100644 --- a/features/rageshake/impl/src/main/res/values-uz/translations.xml +++ b/features/rageshake/impl/src/main/res/values-uz/translations.xml @@ -7,9 +7,11 @@ "Iltimos, muammoni tasvirlab bering. Nima qildingiz? Nima bo\'lishini kutgan edingiz? Aslida nima bo\'ldi. Iltimos, iloji boricha batafsilroq ma\'lumot bering." "Muammoni tasvirlab bering…" "Iloji bo\'lsa, tavsifni ingliz tilida yozing." + "Tavsif juda qisqa, nima boʻlganligi haqida batafsilroq maʼlumot bering. Rahmat!" "Buzilish jurnallarini yuboring" "Jurnallarga ruxsat bering" "Ekran tasvirini yuboring" "Har bir narsa to\'ri ishlayotganiga ishonch hosil qilish uchun xabaringizga jurnallar kiritiladi. Xabarni jurnallarsiz yuborish uchun ushbu sozlamani oʻchiring." "%1$soxirgi marta ishlatilganda qulab tushdi. Biz bilan nosozlik hisobotini baham ko\'rmoqchimisiz?" + "Jurnallarni ko'rish" diff --git a/features/rageshake/impl/src/main/res/values/localazy.xml b/features/rageshake/impl/src/main/res/values/localazy.xml index 9c18d37a3b..f6d93c4114 100644 --- a/features/rageshake/impl/src/main/res/values/localazy.xml +++ b/features/rageshake/impl/src/main/res/values/localazy.xml @@ -14,5 +14,7 @@ "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?" + "If you are having issues with notifications, uploading the notification settings can help us pinpoint the root cause." + "Send notification settings" "View logs" 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 027e2fb38c..362b3798d6 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 @@ -225,15 +225,15 @@ class BugReportPresenterTest { assertThat(awaitItem().sending).isEqualTo(AsyncAction.Uninitialized) } } - - private fun TestScope.createPresenter( - bugReporter: BugReporter = FakeBugReporter(), - crashDataStore: CrashDataStore = FakeCrashDataStore(), - screenshotHolder: ScreenshotHolder = FakeScreenshotHolder(), - ) = BugReportPresenter( - bugReporter = bugReporter, - crashDataStore = crashDataStore, - screenshotHolder = screenshotHolder, - appCoroutineScope = this, - ) } + +internal fun TestScope.createPresenter( + bugReporter: BugReporter = FakeBugReporter(), + crashDataStore: CrashDataStore = FakeCrashDataStore(), + screenshotHolder: ScreenshotHolder = FakeScreenshotHolder(), +) = BugReportPresenter( + bugReporter = bugReporter, + crashDataStore = crashDataStore, + screenshotHolder = screenshotHolder, + appCoroutineScope = this, +) diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/DefaultBugReportEntryPointTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/DefaultBugReportEntryPointTest.kt new file mode 100644 index 0000000000..23d74f7247 --- /dev/null +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/DefaultBugReportEntryPointTest.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.rageshake.impl.bugreport + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint +import io.element.android.features.viewfolder.api.ViewFolderEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class DefaultBugReportEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() = runTest { + val entryPoint = DefaultBugReportEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + BugReportFlowNode( + buildContext = buildContext, + plugins = plugins, + viewFolderEntryPoint = object : ViewFolderEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + ) + } + val callback = object : BugReportEntryPoint.Callback { + override fun onDone() = lambdaError() + } + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .callback(callback) + .build() + assertThat(result).isInstanceOf(BugReportFlowNode::class.java) + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt index f9f1aa72e4..f19362f99c 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt @@ -9,28 +9,28 @@ package io.element.android.features.rageshake.impl.crash import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -import org.robolectric.RuntimeEnvironment @RunWith(RobolectricTestRunner::class) class VectorUncaughtExceptionHandlerTest { @Test fun `activate should change the default handler`() { - val sut = VectorUncaughtExceptionHandler(RuntimeEnvironment.getApplication()) + val sut = VectorUncaughtExceptionHandler(PreferencesCrashDataStore(FakePreferenceDataStoreFactory())) sut.activate() assertThat(Thread.getDefaultUncaughtExceptionHandler()).isInstanceOf(VectorUncaughtExceptionHandler::class.java) } @Test fun `uncaught exception`() = runTest { - val crashDataStore = PreferencesCrashDataStore(RuntimeEnvironment.getApplication()) + val crashDataStore = PreferencesCrashDataStore(FakePreferenceDataStoreFactory()) assertThat(crashDataStore.appHasCrashed().first()).isFalse() assertThat(crashDataStore.crashInfo().first()).isEmpty() - val sut = VectorUncaughtExceptionHandler(RuntimeEnvironment.getApplication()) + val sut = VectorUncaughtExceptionHandler(crashDataStore) sut.uncaughtException(Thread(), AN_EXCEPTION) assertThat(crashDataStore.appHasCrashed().first()).isTrue() val crashInfo = crashDataStore.crashInfo().first() 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 1a9592739d..ecb8d13b12 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 @@ -27,7 +27,7 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.tracing.FakeTracingService import io.element.android.libraries.network.useragent.DefaultUserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionStore -import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers @@ -104,9 +104,9 @@ class DefaultBugReporterTest { ) server.start() - val mockSessionStore = InMemorySessionStore().apply { - storeData(aSessionData(sessionId = "@foo:example.com", deviceId = "ABCDEFGH")) - } + val mockSessionStore = InMemorySessionStore( + initialList = listOf(aSessionData(sessionId = "@foo:example.com", deviceId = "ABCDEFGH")) + ) val fakeEncryptionService = FakeEncryptionService() val matrixClient = FakeMatrixClient(encryptionService = fakeEncryptionService) @@ -165,9 +165,9 @@ class DefaultBugReporterTest { ) server.start() - val mockSessionStore = InMemorySessionStore().apply { - storeData(aSessionData("@foo:example.com", "ABCDEFGH")) - } + val mockSessionStore = InMemorySessionStore( + initialList = listOf(aSessionData("@foo:example.com", "ABCDEFGH")) + ) val fakeEncryptionService = FakeEncryptionService() val matrixClient = FakeMatrixClient(encryptionService = fakeEncryptionService) @@ -308,9 +308,9 @@ class DefaultBugReporterTest { fun `the log directory is initialized using the last session store data`() = runTest { val sut = createDefaultBugReporter( buildMeta = aBuildMeta(isEnterpriseBuild = true), - sessionStore = InMemorySessionStore().apply { - storeData(aSessionData(sessionId = "@alice:domain.com")) - } + sessionStore = InMemorySessionStore( + initialList = listOf(aSessionData(sessionId = "@alice:domain.com")) + ) ) assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs/domain.com") } @@ -318,9 +318,9 @@ class DefaultBugReporterTest { @Test fun `foss build - the log directory is initialized to the root log directory`() = runTest { val sut = createDefaultBugReporter( - sessionStore = InMemorySessionStore().apply { - storeData(aSessionData(sessionId = "@alice:domain.com")) - } + sessionStore = InMemorySessionStore( + initialList = listOf(aSessionData(sessionId = "@alice:domain.com")) + ) ) assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs") } 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 index 7f531fb9e3..1eff7f8206 100644 --- 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 @@ -12,6 +12,6 @@ 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 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 index d60cc76f8d..99d65612bd 100644 --- a/features/reportroom/impl/build.gradle.kts +++ b/features/reportroom/impl/build.gradle.kts @@ -5,7 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies plugins { id("io.element.android-compose-library") @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.reportroom.api) @@ -32,14 +33,6 @@ dependencies { 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) + testCommonDependencies(libs, true) 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 index d3131040d9..e433c70bf6 100644 --- 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 @@ -9,15 +9,16 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 { +@Inject +class DefaultReportRoomEntryPoint : 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 index 55ccb25417..dae5c4e272 100644 --- 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 @@ -7,11 +7,11 @@ package io.element.android.features.reportroom.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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( @@ -29,7 +29,8 @@ interface ReportRoom { } @ContributesBinding(SessionScope::class) -class DefaultReportRoom @Inject constructor( +@Inject +class DefaultReportRoom( private val client: MatrixClient, ) : ReportRoom { override suspend operator fun invoke( 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 index 0c24d6db74..cd136efcba 100644 --- 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 @@ -12,16 +12,17 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId @ContributesNode(SessionScope::class) -class ReportRoomNode @AssistedInject constructor( +@AssistedInject +class ReportRoomNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: ReportRoomPresenter.Factory, 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 index 30ccb9e20c..42ab1cf08a 100644 --- 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 @@ -15,9 +15,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState @@ -25,12 +25,13 @@ import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class ReportRoomPresenter @AssistedInject constructor( +@AssistedInject +class ReportRoomPresenter( @Assisted private val roomId: RoomId, private val reportRoom: ReportRoom, ) : Presenter { @AssistedFactory - interface Factory { + fun interface Factory { fun create(roomId: RoomId): ReportRoomPresenter } diff --git a/features/reportroom/impl/src/main/res/values-de/translations.xml b/features/reportroom/impl/src/main/res/values-de/translations.xml index 6d379818d6..fe7adff258 100644 --- a/features/reportroom/impl/src/main/res/values-de/translations.xml +++ b/features/reportroom/impl/src/main/res/values-de/translations.xml @@ -1,8 +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" + "Deine Meldung wurde erfolgreich übermittelt. Beim Versuch, den Chat zu verlassen, ist allerdings ein Problem aufgetreten. Bitte versuche es erneut." + "Der Chat kann nicht verlassen werden" + "Melde diesen Chat deinem Administrator. Wenn die Nachrichten verschlüsselt sind, kann dein Administrator sie nicht lesen." + "Beschreibe den Grund für die Meldung…" + "Chat melden" diff --git a/features/reportroom/impl/src/main/res/values-ko/translations.xml b/features/reportroom/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..30096de314 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,8 @@ + + + "신고가 성공적으로 제출되었지만, 방을 나가려고 하는 중에 문제가 발생했습니다. 다시 시도해 주세요." + "방을 나갈 수 없습니다" + "이 방을 관리자에게 신고하세요. 메시지가 암호화되어 있는 경우, 관리자는 메시지를 읽을 수 없습니다." + "신고 사유를 설명하세요…" + "방 신고" + 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 index 53f19677ef..2dd386fae9 100644 --- a/features/reportroom/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/reportroom/impl/src/main/res/values-pt-rBR/translations.xml @@ -1,7 +1,7 @@ - "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" + "Sua denúncia foi enviada com sucesso, mas encontramos um problema ao tentar sair da sala. Tente novamente." + "Não foi possível sair da 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 index f83626a9e4..9d11026a13 100644 --- a/features/reportroom/impl/src/main/res/values-pt/translations.xml +++ b/features/reportroom/impl/src/main/res/values-pt/translations.xml @@ -3,6 +3,6 @@ "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…" + "Descreve a razão para denunciar…" "Denunciar sala" diff --git a/features/reportroom/impl/src/main/res/values-ro/translations.xml b/features/reportroom/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..c96d1707ab --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,8 @@ + + + "Raportul dumneavoastră a fost trimis cu succes, dar am întâmpinat o problemă în timp ce încercam să părăsim camera. Vă rugăm să încercați din nou." + "Nu s-a putut părăsi camera" + "Raportați această cameră administratorului. Dacă mesaje sunt criptate, administratorul nu le va putea citi." + "Descrieți motivul raportării…" + "Raportați camera" + diff --git a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomEntryPointTest.kt b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomEntryPointTest.kt new file mode 100644 index 0000000000..c9f850062e --- /dev/null +++ b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomEntryPointTest.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.reportroom.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultReportRoomEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultReportRoomEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + ReportRoomNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { roomId -> + assertThat(roomId).isEqualTo(A_ROOM_ID) + createReportRoomPresenter() + } + ) + } + val result = entryPoint.createNode(parentNode, BuildContext.root(null), A_ROOM_ID) + assertThat(result).isInstanceOf(ReportRoomNode::class.java) + assertThat(result.plugins).contains(ReportRoomNode.Inputs(A_ROOM_ID)) + } +} 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 index d85f2b86e8..eb2e94366d 100644 --- 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 @@ -141,11 +141,11 @@ class ReportRoomPresenterTest { ) } } - - fun createReportRoomPresenter( - roomId: RoomId = A_ROOM_ID, - reportRoom: ReportRoom = FakeReportRoom() - ): ReportRoomPresenter { - return ReportRoomPresenter(roomId, reportRoom) - } +} + +internal fun createReportRoomPresenter( + roomId: RoomId = A_ROOM_ID, + reportRoom: ReportRoom = FakeReportRoom() +): ReportRoomPresenter { + return ReportRoomPresenter(roomId, reportRoom) } diff --git a/features/roomaliasresolver/impl/build.gradle.kts b/features/roomaliasresolver/impl/build.gradle.kts index dcb14f6162..45cf32f661 100644 --- a/features/roomaliasresolver/impl/build.gradle.kts +++ b/features/roomaliasresolver/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2024 New Vector Ltd. @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.roomaliasresolver.api) @@ -33,14 +34,6 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/DefaultRoomAliasResolverEntryPoint.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/DefaultRoomAliasResolverEntryPoint.kt index 9b86ba368c..9c2d3d2cdf 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/DefaultRoomAliasResolverEntryPoint.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/DefaultRoomAliasResolverEntryPoint.kt @@ -10,14 +10,15 @@ package io.element.android.features.roomaliasresolver.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultRoomAliasResolverEntryPoint @Inject constructor() : RoomAliasResolverEntryPoint { +@Inject +class DefaultRoomAliasResolverEntryPoint : RoomAliasResolverEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomAliasResolverEntryPoint.NodeBuilder { val plugins = ArrayList() diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverNode.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverNode.kt index d72158fb9c..d7b3242def 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverNode.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverNode.kt @@ -13,16 +13,17 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias @ContributesNode(SessionScope::class) -class RoomAliasResolverNode @AssistedInject constructor( +@AssistedInject +class RoomAliasResolverNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: RoomAliasResolverPresenter.Factory, 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 34c655bd68..c8a8d18fbc 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 @@ -13,8 +13,8 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState @@ -25,11 +25,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlin.jvm.optionals.getOrElse -class RoomAliasResolverPresenter @AssistedInject constructor( +@AssistedInject +class RoomAliasResolverPresenter( @Assisted private val roomAlias: RoomAlias, private val matrixClient: MatrixClient, ) : Presenter { - interface Factory { + fun interface Factory { fun create( roomAlias: RoomAlias, ): RoomAliasResolverPresenter diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverView.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverView.kt index ef2cc47ecc..9d7536b2f5 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverView.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverView.kt @@ -117,7 +117,7 @@ private fun RoomAliasResolverContent( RoomPreviewOrganism( modifier = modifier, avatar = { - PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) + PlaceholderAtom(width = AvatarSize.RoomPreviewHeader.dp, height = AvatarSize.RoomPreviewHeader.dp) }, title = { RoomPreviewSubtitleAtom(roomAlias.value) diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/di/RoomAliasResolverModule.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/di/RoomAliasResolverModule.kt index 9846169eda..cce26ec602 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/di/RoomAliasResolverModule.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/di/RoomAliasResolverModule.kt @@ -7,15 +7,15 @@ package io.element.android.features.roomaliasresolver.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides import io.element.android.features.roomaliasresolver.impl.RoomAliasResolverPresenter import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias -@Module +@BindingContainer @ContributesTo(SessionScope::class) object RoomAliasResolverModule { @Provides 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 6faabb5b11..60bd1a2bf3 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-de/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-de/translations.xml @@ -1,5 +1,5 @@ - "Wir konnten diese Chatroomvorschau nicht anzeigen" - "Der Raum-Alias konnte nicht ermittelt werden." + "Wir konnten diese Chat-Vorschau nicht anzeigen" + "Der Chat-Alias konnte nicht ermittelt werden." diff --git a/features/roomaliasresolver/impl/src/main/res/values-ko/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..339fa423a0 --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,5 @@ + + + "이 방 미리보기를 표시할 수 없습니다." + "방 별칭을 확인할 수 없습니다." + 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 index 461fb64f7c..d8061322c3 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-pt-rBR/translations.xml @@ -1,5 +1,5 @@ - "Não foi possível exibir a visualização desta sala" + "Não foi possível exibir a pré-visualização desta sala" "Falha ao descobrir o alias da sala." diff --git a/features/roomaliasresolver/impl/src/main/res/values-ro/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-ro/translations.xml index fbec806c6a..195a947871 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-ro/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-ro/translations.xml @@ -1,4 +1,5 @@ + "Nu am putut afișa previzualizarea acestei camere." "Nu s-a putut rezolva alias-ul camerei." diff --git a/features/roomaliasresolver/impl/src/main/res/values-uz/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-uz/translations.xml new file mode 100644 index 0000000000..0499fed4f5 --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-uz/translations.xml @@ -0,0 +1,4 @@ + + + "Xona taxalluslari yechilmadi." + diff --git a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/DefaultRoomAliasResolverEntryPointTest.kt b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/DefaultRoomAliasResolverEntryPointTest.kt new file mode 100644 index 0000000000..238b35017b --- /dev/null +++ b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/DefaultRoomAliasResolverEntryPointTest.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.roomaliasresolver.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias +import io.element.android.libraries.matrix.test.A_ROOM_ALIAS +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultRoomAliasResolverEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultRoomAliasResolverEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + RoomAliasResolverNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { alias -> + assertThat(alias).isEqualTo(A_ROOM_ALIAS) + createPresenter( + alias, + ) + } + ) + } + val callback = object : RoomAliasResolverEntryPoint.Callback { + override fun onAliasResolved(data: ResolvedRoomAlias) = lambdaError() + } + val params = RoomAliasResolverEntryPoint.Params( + roomAlias = A_ROOM_ALIAS + ) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .callback(callback) + .build() + assertThat(result).isInstanceOf(RoomAliasResolverNode::class.java) + assertThat(result.plugins).contains(params) + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperPresenterTest.kt b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperPresenterTest.kt index 3071d8943a..f90b07c91b 100644 --- a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperPresenterTest.kt +++ b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperPresenterTest.kt @@ -79,16 +79,16 @@ class RoomAliasHelperPresenterTest { assertThat(retryState.resolveState.errorOrNull()).isEqualTo(AN_EXCEPTION) } } - - private fun createPresenter( - roomAlias: RoomAlias = A_ROOM_ALIAS, - matrixClient: MatrixClient = FakeMatrixClient(), - ) = RoomAliasResolverPresenter( - roomAlias = roomAlias, - matrixClient = matrixClient, - ) } +internal fun createPresenter( + roomAlias: RoomAlias = A_ROOM_ALIAS, + matrixClient: MatrixClient = FakeMatrixClient(), +) = RoomAliasResolverPresenter( + roomAlias = roomAlias, + matrixClient = matrixClient, +) + internal fun aResolvedRoomAlias( roomId: RoomId = A_ROOM_ID, servers: List = A_SERVER_LIST, diff --git a/features/roomcall/impl/build.gradle.kts b/features/roomcall/impl/build.gradle.kts index 0ec555bfb5..7a6a3cf0a6 100644 --- a/features/roomcall/impl/build.gradle.kts +++ b/features/roomcall/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2024 New Vector Ltd. @@ -15,7 +16,7 @@ android { namespace = "io.element.android.features.roomcall.impl" } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.roomcall.api) @@ -26,15 +27,8 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs, true) 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 47af2c24fc..2401606f34 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 @@ -13,6 +13,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import dev.zacsweers.metro.Inject import io.element.android.features.call.api.CurrentCall import io.element.android.features.call.api.CurrentCallService import io.element.android.features.enterprise.api.SessionEnterpriseService @@ -20,9 +21,9 @@ import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.architecture.Presenter 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( +@Inject +class RoomCallStatePresenter( private val room: JoinedRoom, private val currentCallService: CurrentCallService, private val sessionEnterpriseService: SessionEnterpriseService, diff --git a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/di/RoomCallModule.kt b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/di/RoomCallModule.kt index cd02ae8b7d..3a8ac51ef6 100644 --- a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/di/RoomCallModule.kt +++ b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/di/RoomCallModule.kt @@ -7,16 +7,16 @@ package io.element.android.features.roomcall.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.impl.RoomCallStatePresenter import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope @ContributesTo(RoomScope::class) -@Module +@BindingContainer interface RoomCallModule { @Binds fun bindRoomCallStatePresenter(presenter: RoomCallStatePresenter): Presenter diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 302ae7d8c6..882058ef48 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.appconfig) @@ -58,13 +59,7 @@ dependencies { implementation(projects.features.changeroommemberroles.api) implementation(projects.features.invitepeople.api) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.test.mockk) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.mediapickers.test) @@ -72,9 +67,6 @@ dependencies { testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.usersearch.test) testImplementation(projects.libraries.featureflag.test) - testImplementation(projects.tests.testutils) testImplementation(projects.features.startchat.test) testImplementation(projects.services.analytics.test) - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt index ea5a0873e0..6d26ef32a1 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt @@ -10,16 +10,17 @@ package io.element.android.features.roomdetails.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint.InitialTarget import io.element.android.features.roomdetails.impl.RoomDetailsFlowNode.NavTarget import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultRoomDetailsEntryPoint @Inject constructor() : RoomDetailsEntryPoint { +@Inject +class DefaultRoomDetailsEntryPoint : RoomDetailsEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDetailsEntryPoint.NodeBuilder { return object : RoomDetailsEntryPoint.NodeBuilder { val plugins = ArrayList() 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 a802d5cd0a..2b4bf10d67 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 @@ -20,10 +20,10 @@ import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.Interaction -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.appconfig.LearnMoreConfig import io.element.android.features.call.api.CallType import io.element.android.features.call.api.ElementCallEntryPoint @@ -67,7 +67,8 @@ import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) -class RoomDetailsFlowNode @AssistedInject constructor( +@AssistedInject +class RoomDetailsFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val pollHistoryEntryPoint: PollHistoryEntryPoint, 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 d9df8cbc1c..2ada71dbc8 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 @@ -19,10 +19,10 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.features.leaveroom.api.LeaveRoomRenderer import io.element.android.libraries.androidutils.system.startSharePlainTextIntent import io.element.android.libraries.architecture.appyx.launchMolecule @@ -36,7 +36,8 @@ import timber.log.Timber import io.element.android.libraries.androidutils.R as AndroidUtilsR @ContributesNode(RoomScope::class) -class RoomDetailsNode @AssistedInject constructor( +@AssistedInject +class RoomDetailsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: RoomDetailsPresenter, 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 026b616996..0071aced5d 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 @@ -15,6 +15,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState @@ -55,9 +56,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import javax.inject.Inject -class RoomDetailsPresenter @Inject constructor( +@Inject +class RoomDetailsPresenter( private val client: MatrixClient, private val room: JoinedRoom, private val featureFlagService: FeatureFlagService, 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 e41392ee76..c9087d6458 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 @@ -67,7 +67,6 @@ fun aDmRoomMember( membership: RoomMembershipState = RoomMembershipState.JOIN, isNameAmbiguous: Boolean = false, powerLevel: Long = 0, - normalizedPowerLevel: Long = powerLevel, isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, @@ -78,7 +77,6 @@ fun aDmRoomMember( membership = membership, isNameAmbiguous = isNameAmbiguous, powerLevel = powerLevel, - normalizedPowerLevel = normalizedPowerLevel, isIgnored = isIgnored, role = role, membershipChangeReason = membershipChangeReason 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 3fdb2a0800..61d5ba115a 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 @@ -396,10 +396,10 @@ private fun RoomHeaderSection( horizontalAlignment = Alignment.CenterHorizontally, ) { Avatar( - avatarData = AvatarData(roomId.value, roomName, avatarUrl, AvatarSize.RoomHeader), + avatarData = AvatarData(roomId.value, roomName, avatarUrl, AvatarSize.RoomDetailsHeader), avatarType = AvatarType.Room( heroes = heroes.map { user -> - user.getAvatarData(size = AvatarSize.RoomHeader) + user.getAvatarData(size = AvatarSize.RoomDetailsHeader) }.toPersistentList(), isTombstoned = isTombstoned, ), 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 15afcbc99f..9ca7b87c00 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 @@ -7,9 +7,9 @@ package io.element.android.features.roomdetails.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.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 @@ -18,7 +18,7 @@ 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.JoinedRoom -@Module +@BindingContainer @ContributesTo(RoomScope::class) object RoomMemberModule { @Provides diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditNode.kt index 8cd53405f5..8143f1848f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditNode.kt @@ -13,15 +13,16 @@ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.RoomScope import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(RoomScope::class) -class RoomDetailsEditNode @AssistedInject constructor( +@AssistedInject +class RoomDetailsEditNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: RoomDetailsEditPresenter, 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 ae324b4027..2520d0d29c 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 @@ -20,6 +20,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.core.net.toUri +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.file.TemporaryUriDeleter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter @@ -42,9 +43,9 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject -class RoomDetailsEditPresenter @Inject constructor( +@Inject +class RoomDetailsEditPresenter( private val room: JoinedRoom, private val mediaPickerProvider: PickerProvider, private val mediaPreProcessor: MediaPreProcessor, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt index dc269e7322..3c3acc7b4d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt @@ -8,15 +8,16 @@ package io.element.android.features.roomdetails.impl.invite import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect 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.plugin.Plugin -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.features.invitepeople.api.InvitePeoplePresenter import io.element.android.features.invitepeople.api.InvitePeopleRenderer import io.element.android.libraries.di.RoomScope @@ -24,7 +25,8 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(RoomScope::class) -class RoomInviteMembersNode @AssistedInject constructor( +@AssistedInject +class RoomInviteMembersNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val analyticsService: AnalyticsService, @@ -48,11 +50,18 @@ class RoomInviteMembersNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { val state = invitePeoplePresenter.present() + + // Once invites have been sent successfully, close the Invite view. + LaunchedEffect(state.sendInvitesAction) { + if (state.sendInvitesAction.isReady()) { + navigateUp() + } + } + RoomInviteMembersView( state = state, modifier = modifier, - onBackClick = { navigateUp() }, - onDone = { navigateUp() } + onBackClick = { navigateUp() } ) { invitePeopleRenderer.Render(state, Modifier) } 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 8bec90707f..f7991a6e44 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 @@ -8,22 +8,29 @@ package io.element.android.features.roomdetails.impl.invite import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets 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.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.features.invitepeople.api.InvitePeopleEvents import io.element.android.features.invitepeople.api.InvitePeopleState import io.element.android.features.invitepeople.api.InvitePeopleStateProvider import io.element.android.features.roomdetails.impl.R +import io.element.android.libraries.designsystem.components.ProgressDialog 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.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 @@ -32,7 +39,6 @@ import io.element.android.libraries.ui.strings.CommonStrings fun RoomInviteMembersView( state: InvitePeopleState, onBackClick: () -> Unit, - onDone: () -> Unit, modifier: Modifier = Modifier, invitePeopleView: @Composable () -> Unit, ) { @@ -49,7 +55,6 @@ fun RoomInviteMembersView( }, onSubmitClick = { state.eventSink(InvitePeopleEvents.SendInvites) - onDone() }, canSend = state.canInvite, ) @@ -64,6 +69,10 @@ fun RoomInviteMembersView( invitePeopleView() } } + + if (state.sendInvitesAction.isLoading()) { + InviteProgressDialog() + } } @OptIn(ExperimentalMaterial3Api::class) @@ -86,6 +95,24 @@ private fun RoomInviteMembersTopBar( ) } +@Composable +private fun InviteProgressDialog() { + ProgressDialog { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.screen_room_details_invite_people_preparing), + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontHeadingSmMedium, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(R.string.screen_room_details_invite_people_dont_close), + color = ElementTheme.colors.textSecondary, + style = MaterialTheme.typography.bodyMedium, + ) + } +} + @PreviewsDayNight @Composable internal fun RoomInviteMembersViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview { @@ -93,6 +120,5 @@ internal fun RoomInviteMembersViewPreview(@PreviewParameter(InvitePeopleStatePro state = state, invitePeopleView = {}, onBackClick = {}, - onDone = {}, ) } 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 ef627a4992..bbb231ccb9 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 @@ -7,15 +7,16 @@ package io.element.android.features.roomdetails.impl.members +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers 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( +@Inject +class RoomMemberListDataSource( private val room: BaseRoom, private val coroutineDispatchers: CoroutineDispatchers, ) { 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 3d7fecd0ae..cc7a6e2151 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 @@ -14,10 +14,10 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.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 @@ -26,7 +26,8 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(RoomScope::class) -class RoomMemberListNode @AssistedInject constructor( +@AssistedInject +class RoomMemberListNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: RoomMemberListPresenter, 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 fac98da36f..c5bd507d69 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 @@ -16,6 +16,7 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.features.roommembermoderation.api.RoomMemberModerationState @@ -38,15 +39,16 @@ import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentMap +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext -import javax.inject.Inject -class RoomMemberListPresenter @Inject constructor( +@Inject +class RoomMemberListPresenter( private val room: JoinedRoom, private val roomMemberListDataSource: RoomMemberListDataSource, private val coroutineDispatchers: CoroutineDispatchers, @@ -66,11 +68,6 @@ class RoomMemberListPresenter @Inject constructor( val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canInvite by room.canInviteAsState(syncUpdateFlow.value) val roomModerationState = roomMembersModerationPresenter.present() - val activeRoomMemberCount by produceState(0L) { - room.roomInfoFlow.map { it.activeMembersCount } - .distinctUntilChanged() - .collect { value = it } - } val roomMemberIdentityStates by produceState(persistentMapOf()) { room.roomMemberIdentityStateChange(waitForEncryption = true) @@ -81,8 +78,12 @@ class RoomMemberListPresenter @Inject constructor( } // Update the room members when the screen is loaded or the active member count changes - LaunchedEffect(activeRoomMemberCount) { - room.updateMembers() + LaunchedEffect(Unit) { + room.roomInfoFlow.map { it.activeMembersCount } + .distinctUntilChanged() + .collectLatest { + room.updateMembers() + } } LaunchedEffect(membersState, roomMemberIdentityStates) { 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 52acb6ebd6..a1cc9c3b92 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 @@ -148,7 +148,6 @@ fun aRoomMember( membership: RoomMembershipState = RoomMembershipState.JOIN, isNameAmbiguous: Boolean = false, powerLevel: Long = 0L, - normalizedPowerLevel: Long = 0L, isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, @@ -159,7 +158,6 @@ fun aRoomMember( membership = membership, isNameAmbiguous = isNameAmbiguous, powerLevel = powerLevel, - normalizedPowerLevel = normalizedPowerLevel, isIgnored = isIgnored, role = role, membershipChangeReason = membershipChangeReason, 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 6a1af42694..3b364c6f92 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 @@ -14,10 +14,10 @@ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.features.userprofile.shared.UserProfileNodeHelper import io.element.android.features.userprofile.shared.UserProfileView import io.element.android.libraries.architecture.NodeInputs @@ -29,7 +29,8 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(RoomScope::class) -class RoomMemberDetailsNode @AssistedInject constructor( +@AssistedInject +class RoomMemberDetailsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val analyticsService: AnalyticsService, 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 d2d5db00e9..d5cf9e85cb 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 @@ -14,8 +14,8 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.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 @@ -41,7 +41,8 @@ import kotlinx.coroutines.launch * Presenter for room member details screen. * Rely on UserProfilePresenter, but override some fields with room member info when available. */ -class RoomMemberDetailsPresenter @AssistedInject constructor( +@AssistedInject +class RoomMemberDetailsPresenter( @Assisted private val roomMemberId: UserId, private val room: JoinedRoom, private val encryptionService: EncryptionService, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt index dc2660f566..0e5a9b23b1 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt @@ -14,17 +14,18 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode 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 @ContributesNode(RoomScope::class) -class RoomNotificationSettingsNode @AssistedInject constructor( +@AssistedInject +class RoomNotificationSettingsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: RoomNotificationSettingsPresenter.Factory, 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 fb7a3b03da..413e71169a 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 @@ -17,9 +17,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -37,7 +37,8 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.seconds -class RoomNotificationSettingsPresenter @AssistedInject constructor( +@AssistedInject +class RoomNotificationSettingsPresenter( private val room: JoinedRoom, private val notificationSettingsService: NotificationSettingsService, @Assisted private val showUserDefinedSettingStyle: Boolean, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsFlowNode.kt index dbe1eec70a..62689489eb 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsFlowNode.kt @@ -17,9 +17,9 @@ import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsNode @@ -33,7 +33,8 @@ import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) -class RolesAndPermissionsFlowNode @AssistedInject constructor( +@AssistedInject +class RolesAndPermissionsFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint, 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 80e2d007a3..a430b0f6a5 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 @@ -15,9 +15,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.RoomMember @@ -29,7 +29,8 @@ import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch @ContributesNode(RoomScope::class) -class RolesAndPermissionsNode @AssistedInject constructor( +@AssistedInject +class RolesAndPermissionsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: RolesAndPermissionsPresenter, 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 df62dbbbc0..2ad4c84028 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 @@ -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 dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.RoomModeration import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter @@ -30,9 +31,9 @@ import io.element.android.libraries.matrix.ui.model.roleOf import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import javax.inject.Inject -class RolesAndPermissionsPresenter @Inject constructor( +@Inject +class RolesAndPermissionsPresenter( private val room: JoinedRoom, private val dispatchers: CoroutineDispatchers, private val analyticsService: AnalyticsService, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsNode.kt index 36c3619d38..cebcc56e7f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsNode.kt @@ -13,16 +13,17 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) -class ChangeRoomPermissionsNode @AssistedInject constructor( +@AssistedInject +class ChangeRoomPermissionsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: ChangeRoomPermissionsPresenter.Factory, @@ -33,10 +34,7 @@ class ChangeRoomPermissionsNode @AssistedInject constructor( ) : NodeInputs, Parcelable private val inputs: Inputs = inputs() - - private val presenter = presenterFactory.run { - create(inputs.section) - } + private val presenter = presenterFactory.create(inputs.section) @Composable override fun View(modifier: Modifier) { 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 8b6f7efc96..ec90df1d5e 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 @@ -15,9 +15,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.roomdetails.impl.analytics.trackPermissionChangeAnalytics import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter @@ -29,7 +29,8 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class ChangeRoomPermissionsPresenter @AssistedInject constructor( +@AssistedInject +class ChangeRoomPermissionsPresenter( @Assisted private val section: ChangeRoomPermissionsSection, private val room: JoinedRoom, private val analyticsService: AnalyticsService, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt index 22fecf6143..a0295dde36 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyFlowNode.kt @@ -14,9 +14,9 @@ import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressNode import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode @@ -25,7 +25,8 @@ import io.element.android.libraries.di.RoomScope import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) -class SecurityAndPrivacyFlowNode @AssistedInject constructor( +@AssistedInject +class SecurityAndPrivacyFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, ) : BaseFlowNode( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt index 537306f44f..15580aab6a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNode.kt @@ -13,13 +13,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.RoomScope @ContributesNode(RoomScope::class) -class SecurityAndPrivacyNode @AssistedInject constructor( +@AssistedInject +class SecurityAndPrivacyNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: SecurityAndPrivacyPresenter.Factory, 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 35ecbc0d51..abc0ff72af 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 @@ -17,9 +17,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.matchesServer import io.element.android.features.roomdetails.impl.securityandprivacy.permissions.securityAndPrivacyPermissionsAsState import io.element.android.libraries.architecture.AsyncAction @@ -40,7 +40,8 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay import kotlinx.coroutines.launch -class SecurityAndPrivacyPresenter @AssistedInject constructor( +@AssistedInject +class SecurityAndPrivacyPresenter( @Assisted private val navigator: SecurityAndPrivacyNavigator, private val matrixClient: MatrixClient, private val room: JoinedRoom, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt index efdae76b61..76cb1311fc 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt @@ -13,14 +13,15 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator import io.element.android.libraries.di.RoomScope @ContributesNode(RoomScope::class) -class EditRoomAddressNode @AssistedInject constructor( +@AssistedInject +class EditRoomAddressNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: EditRoomAddressPresenter.Factory, 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 32af99dc1d..95aee73c13 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 @@ -16,9 +16,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter @@ -34,7 +34,8 @@ import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class EditRoomAddressPresenter @AssistedInject constructor( +@AssistedInject +class EditRoomAddressPresenter( @Assisted private val navigator: SecurityAndPrivacyNavigator, private val client: MatrixClient, private val room: JoinedRoom, 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 fb01acb4fb..cfe719f190 100644 --- a/features/roomdetails/impl/src/main/res/values-bg/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-bg/translations.xml @@ -1,5 +1,6 @@ + "Възникна грешка при обновяването на настройките за известия." "Вашият сървър не поддържа тази опция в шифровани стаи, може да не получавате известия в някои стаи." "Анкети" "Само администратори" @@ -26,9 +27,13 @@ "Без шифроване" "Общодостъпна стая" "Редактиране на стаята" + "Възникна неизвестна грешка и информацията не можа да бъде променена." "Не може да се обнови стаята" "Съобщенията са защитени с ключове. Само вие и получателите имате уникалните ключове, за да ги отключите." "Шифроването на съобщенията е включено" + "Възникна грешка при зареждането на настройките за известия." + "Неуспешно заглушаване на тази стая, моля, опитайте отново." + "Неуспешно раззаглушаване на тази стая, моля, опитайте отново." "Поканване на хора" "Напускане на разговора" "Напускане на стаята" @@ -40,6 +45,7 @@ "Профил" "Роли и разрешения" "Име на стаята" + "Защита и поверителност" "Защита" "Споделяне на стаята" "Информация за стаята" @@ -53,7 +59,16 @@ "Администратор" "Модератор" "Членове на стаята" + "Разрешаване на персонализирана настройка" + "Включването на това ще замени вашата настройка по подразбиране" "Да бъда известяван в този чат за" + "Можете да го промените във вашите %1$s." + "глобални настройки" + "Настройка по подразбиране" + "Премахване на персонализираната настройка" + "Възникна грешка при зареждането на настройките за известия." + "Неуспешно възстановяване на режима по подразбиране, моля, опитайте отново." + "Неуспешно задаване на режима, моля, опитайте отново." "Всички съобщения" "Само споменавания и ключови думи" "В тази стая, да бъда известяван за" @@ -67,9 +82,25 @@ "Роли" "Подробности за стаята" "Роли и разрешения" + "Добавяне на адрес на стаята" + "Да, включване на шифроването" + "Да се включи ли шифроването?" + "Веднъж включено, шифроването не може да бъде изключено." "Шифроване" + "Включване на шифроване от край до край" + "Всеки може да намери и да се присъедини" "Всеки" + "Хората могат да се присъединят само ако са поканени" + "Само с покана" + "Достъп до стаята" + "Пространствата в момента не се поддържат" + "Членове на пространството" + "Ще ви е необходим адрес на стаята, за да я направите видима в директорията на стаите." "Видима в директорията на обществените стаи" "Всеки" + "Кой може да чете историята" + "Само за членове откакто са поканени" + "Само за членове от избирането на тази опция" "Видимост на стаята" + "Защита и поверителност" 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 d484486940..e5186f1d4f 100644 --- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml @@ -22,13 +22,17 @@ "Upravit správce" "Tuto akci nebudete moci vrátit zpět. Upravujete oprávnění uživatele, tak aby měl stejnou úroveň jako vy." "Přidat správce?" + "Tuto akci nebudete moci vrátit zpět. Převádíte vlastnictví na vybrané uživatele. Jakmile tuto akci opustíte, bude tato změna trvalá." + "Převést vlastnictví?" "Degradovat" "Tuto změnu nebudete moci vrátit zpět, protože sami degradujete, pokud jste posledním privilegovaným uživatelem v místnosti, nebude možné znovu získat oprávnění." "Degradovat se?" "%1$s (čekající)" "(Čeká na vyřízení)" "Správci mají automaticky oprávnění moderátora" + "Vlastníci mají automaticky administrátorská oprávnění." "Upravit moderátory" + "Vyberte vlastníky" "Správci" "Moderátoři" "Členové" @@ -46,6 +50,8 @@ "Při načítání nastavení oznámení došlo k chybě." "Ztišení této místnosti se nezdařilo, zkuste to prosím znovu." "Nepodařilo se zrušit ztišení této místnosti, zkuste to prosím znovu." + "Nezavírejte aplikaci, dokud neskončíte." + "Příprava pozvánek…" "Pozvat přátele" "Opustit konverzaci" "Opustit místnost" @@ -98,12 +104,14 @@ "Pouze zmínky a klíčová slova" "V této místnosti mě upozornit na" "Správci" + "Správci a vlastníci" "Změnit moji roli" "Degradovat na člena" "Degradovat na moderátora" "Moderování členů" "Zprávy a obsah" "Moderátoři" + "Vlastníci" "Oprávnění" "Obnovit oprávnění" "Po obnovení oprávnění ztratíte aktuální nastavení." diff --git a/features/roomdetails/impl/src/main/res/values-cy/translations.xml b/features/roomdetails/impl/src/main/res/values-cy/translations.xml index 80726aa1f8..40c404d69b 100644 --- a/features/roomdetails/impl/src/main/res/values-cy/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cy/translations.xml @@ -7,7 +7,7 @@ "Pleidleisiau" "Gweinyddwyr yn unig" "Gwahardd pobl" - "Dileu negeseuon" + "Tynnu negeseuon" "Pawb" "Gwahodd pobl a derbyn ceisiadau i ymuno" "Cymedroli aelodau" @@ -22,13 +22,17 @@ "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?" + "Fyddwch chi ddim yn gallu dadwneud y weithred hon. Rydych yn trosglwyddo\'r berchnogaeth i\'r defnyddwyr a ddewiswyd. Unwaith y byddwch yn gadael bydd hyn yn barhaol." + "Trosglwyddo perchnogaeth?" "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" + "Mae gan berchnogion freintiau gweinyddwr yn awtomatig." "Golygu Cymedrolwyr" + "Dewiswch Berchnogion" "Gweinyddwyr" "Cymedrolwyr" "Aelodau" @@ -46,6 +50,8 @@ "Digwyddodd gwall wrth lwytho gosodiadau hysbysu." "Wedi methu tewi\'r ystafell hon, ceisiwch eto." "Wedi methu dad-dewi\'r ystafell hon, ceisiwch eto." + "Peidiwch â chau\'r ap nes ei fod wedi gorffen." + "Wrthi\'n paratoi gwahoddiadau…" "Gwahodd pobl" "Gadael y sgwrs" "Gadael yr ystafell" @@ -83,6 +89,7 @@ "Dan ystyriaeth" "Gweinyddwr" "Cymedrolwr" + "Perchennog" "Aelodau\'r ystafell" "Dad-wahardd %1$s" "Caniatáu gosodiad personol" @@ -100,12 +107,14 @@ "Crybwylliadau ac Allweddeiriau\'n unig" "Yn yr ystafell hon, rhowch wybod i mi am" "Gweinyddwyr" + "Gweinyddwyr a pherchnogion" "Newid fy rôl" "Israddio aelod" "Israddio cymedrolwr" "Cymedroli aelodau" "Negeseuon a chynnwys" "Cymedrolwyr" + "Perchnogion" "Caniatâd" "Ailosod caniatâd" "Ar ôl i chi ailosod caniatâd, byddwch yn colli\'r gosodiadau cyfredol." diff --git a/features/roomdetails/impl/src/main/res/values-da/translations.xml b/features/roomdetails/impl/src/main/res/values-da/translations.xml index a07d86be77..77e34d87dc 100644 --- a/features/roomdetails/impl/src/main/res/values-da/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-da/translations.xml @@ -6,7 +6,7 @@ "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" + "Spær brugere" "Fjern beskeder" "Alle" "Invitér personer og acceptér anmodninger om at deltage" @@ -50,7 +50,9 @@ "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" + "Luk ikke appen, før den er færdig." + "Forbereder invitationer…" + "Invitér andre" "Forlad samtalen" "Forlad rum" "Medier og filer" @@ -129,7 +131,7 @@ Vi anbefaler ikke at aktivere kryptering for rum, som alle kan finde og deltage "Aktivér end-to-end-kryptering" "Alle kan finde og deltage" "Enhver" - "Folk kan kun deltage, hvis de bliver inviteret" + "Andre kan kun deltage, hvis de bliver inviteret" "Kun med invitation" "Adgang til rummet" "Klynger understøttes ikke i øjeblikket" 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 714d36c38a..4a4f8a44b6 100644 --- a/features/roomdetails/impl/src/main/res/values-de/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -1,51 +1,57 @@ - "Sie brauchen eine Chatroomadresse, so dass sie im Verzeichnis sichtbar gemacht werden kann." - "Chatroomadresse" + "Du benötigst eine Chat-Adresse, um den Chat im Verzeichnis sichtbar zu machen." + "Chat-Adresse" "Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." - "Ihr Homeserver unterstützt diese Option in verschlüsselten Räumen nicht. In einigen Räumen werden Sie möglicherweise nicht benachrichtigt." + "Dein Homeserver unterstützt diese Option in verschlüsselten Chats nicht. In einigen Chats erhältst du möglicherweise keine Benachrichtigungen." "Umfragen" - "Nur Administratoren" + "Nur Admins" "Mitglieder sperren" - "Nachrichten anderer Mitgliedern löschen" + "Nachrichten entfernen" "Alle" - "Leute einladen und Beitrittsanfragen annehmen" + "Personen einladen und Beitrittsanfragen annehmen" "Moderation der Mitglieder" "Nachrichten senden & löschen" - "Administratoren und Moderatoren" + "Admins und Moderatoren" "Personen entfernen und Beitrittsanfragen ablehnen" "Avatar ändern" - "Raum-Details anpassen" - "Raumname ändern" - "Raumthema ändern" + "Chat-Details anpassen" + "Chat-Namen ändern" + "Chat Thema ändern" "Nachrichten senden" "Admins bearbeiten" - "Sie können diese Aktion nicht mehr rückgängig machen. Sie vergeben die gleiche Rolle, die Sie auch haben." - "Als Administrator hinzufügen?" + "Du kannst diese Aktion nicht mehr rückgängig machen. Du vergibst dieselbe Rolle, die du auch hast." + "Als Admin hinzufügen?" + "Du kannst diese Aktion nicht rückgängig machen. Du überträgst die Eigentumsrechte an die ausgewählten Nutzer. Sobald du diesen Vorgang abschließt, ist er endgültig." + "Eigentumsrechte übertragen?" "Zurückstufen" - "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?" + "Du stufst dich selbst herab. Diese Änderung kann nicht rückgängig gemacht werden. Wenn du der letzte Nutzer mit dieser Rolle bist, ist es nicht möglich, diese Rolle wiederzuerlangen." + "Möchtest du dich selbst herabstufen?" "%1$s (Ausstehend)" "(Ausstehend)" - "Administratoren haben automatisch Moderatorenrechte" + "Admins haben automatisch Moderatorenrechte" + "Eigentümer haben automatisch Adminrechte." "Moderatoren bearbeiten" - "Administratoren" + "Wähle Eigentümer" + "Admins" "Moderatoren" "Mitglieder" - "Sie haben ungespeicherte Änderungen." + "Du hast nicht gespeicherte Änderungen." "Änderungen speichern?" "Thema hinzufügen" "Verschlüsselt" "Nicht verschlüsselt" - "Öffentlicher Raum" - "Raum bearbeiten" + "Öffentlicher Chat" + "Chat bearbeiten" "Es ist ein unbekannter Fehler aufgetreten und die Informationen konnten nicht geändert werden." - "Raum kann nicht aktualisiert werden" - "Nachrichten und Anrufe sind verschlüsselt. Nur Sie und die Empfänger haben die Schlüssel, um sie zu entsperren." + "Chat 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." "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." + "Schließ die App erst, wenn du fertig bist." + "Einladungen werden vorbereitet…" "Nutzer einladen" "Unterhaltung verlassen" "Verlassen" @@ -57,14 +63,14 @@ "Profil" "Beitrittsanfragen" "Rollen und Berechtigungen" - "Raumname" + "Chat-Name" "Sicherheit & Datenschutz" "Sicherheit" "Teilen" "Informationen" "Thema" - "Raum wird aktualisiert…" - "In diesem Chatroom gibt es keine gesperrten Nutzer." + "Chat wird aktualisiert…" + "In diesem Chat gibt es keine gesperrten Nutzer." "%1$d Person" "%1$d Personen" @@ -72,17 +78,18 @@ "Mitglied entfernen und sperren" "Mitglied nur entfernen" "Sperre aufheben" - "Die Nutzer können den Raum wieder beitreten, wenn sie dazu eingeladen werden." + "Die Nutzer können dem Chat wieder beitreten, wenn sie eingeladen werden." "Nutzer entsperren" "Gesperrt" "Mitglieder" "Ausstehend" - "Administrator" + "Admin" "Moderator" + "Eigentümer" "Mitglieder" "%1$s wird entsperrt." "Benutzerdefinierte Einstellungen verwenden" - "Dies wird ihre Standardeinstellungen außer Kraft setzen." + "Wenn du dies einschaltest, werden deine Standardeinstellungen außer Kraft setzen." "Benachrichtige mich in diesem Chat bei" "Zum Anpassen der Standardeinstellungen gehe zu: %1$s" "Globale Einstellungen" @@ -91,54 +98,57 @@ "Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." "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." + "Dein Homeserver unterstützt diese Option in verschlüsselten Chats nicht. Du erhältst in diesem Chat keine Benachrichtigungen." "Alle Nachrichten" "Nur Erwähnungen und Schlüsselwörter" "Benachrichtige mich bei" - "Administratoren" + "Admins" + "Admins und Eigentümer" "Ändere meine Rolle" "Zum Mitglied herabstufen" "Zum Moderator herabstufen" "Moderation der Mitglieder" "Nachrichten senden & löschen" "Moderatoren" + "Eigentümer" "Berechtigungen" "Rollen und Berechtigungen zurücksetzen" - "Sobald Sie die Berechtigungen zurücksetzen, verlieren Sie die aktuellen Einstellungen." + "Sobald du die Berechtigungen zurücksetzt, verlierst du die aktuellen Einstellungen." "Berechtigungen zurücksetzen?" "Rollen" - "Raum-Details anpassen" + "Chat-Details anpassen" "Rollen und Berechtigungen" - "Chatroomadresse hinzufügen" - "Jeder kann den Zutritt zum Raum beantragen, aber ein Administrator oder ein Moderator müssen die Anfrage akzeptieren." + "Chat-Adresse hinzufügen" + "Jeder kann den Beitritt zum Chat anfragen, aber ein Admin oder Moderator müssen die Anfrage akzeptieren." "Beitritt beantragen" "Ja, Verschlüsselung aktivieren" - "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." + "Einmal angeschaltet kann die Verschlüsselung für einen Chat nicht mehr deaktiviert werden. Der Nachrichtenverlauf ist für Mitglieder nur sichtbar, seit sie eingeladen wurden oder dem Chat beigetreten sind. +Niemand außer den Chat Mitgliedern kann Nachrichten lesen. Dies kann verhindern, dass Bots und Bridges richtig funktionieren. +Wir empfehlen keine Verschlüsselung für Chats zu aktivieren, die jeder finden und denen jeder beitreten darf." "Verschlüsselung aktivieren?" "Einmal angeschaltet kann die Verschlüsselung nicht mehr deaktiviert werden." "Verschlüsselung" "Ende-zu-Ende-Verschlüsselung aktivieren" - "Jeder kann diesen Raum finden und betreten" + "Jeder kann diesen Chat finden und ihm beitreten" "Jeder" "Personen können nur beitreten, wenn sie eingeladen werden." "Nur auf Einladung" - "Chatroomzugang" + "Chat Zugang" "Spaces werden zur Zeit nicht unterstützt." "Spacemitglieder" - "Um den Chatroom im Chatroomverzeichnis sichtbar zu machen, benötigen Sie eine Chatroomadresse." - "Chatroomadresse" - "Erlauben Sie, dass dieser Chatroom im öffentlichen Chatroomverzeichnis von %1$s gefunden werden kann." - "Sichtbar im öffentlichen Chatroomverzeichnis" + "Du benötigst eine Chat-Adresse, um den Chat im Verzeichnis sichtbar zu machen." + "Chat-Adresse" + "Erlaube das Auffinden dieses Chats durch Suche im öffentlichen Verzeichnis von %1$s" + "Sichtbar im öffentlichen Verzeichnis" "Jeder" - "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." - " Sichtbarkeit des Chatrooms" + "Wer hat Zugriff auf den Nachrichtenverlauf" + "Nur Mitglieder, aber erst seit deren Einladung" + "Nur Mitglieder seit Auswahl dieser Option" + "Chat-Adressen machen es möglich, Chats zu finden und ihnen beizutreten. Dies erleichtert es, Chats mit anderen zu teilen. +Auf Wunsch kannst du deinen Chat im öffentlichen Verzeichnis deines Homeservers veröffentlichen." + "Veröffentlichung von Chats" + "Chat-Adressen machen es möglich, Chats zu finden und ihnen beizutreten. Dies erleichtert es, Chats mit anderen zu teilen. +Die Adresse ist auch erforderlich, um den Chat im öffentlichen Verzeichnis von %1$s zu veröffentlichen." + " Sichtbarkeit des Chats" "Sicherheit & Datenschutz" diff --git a/features/roomdetails/impl/src/main/res/values-eo/translations.xml b/features/roomdetails/impl/src/main/res/values-eo/translations.xml new file mode 100644 index 0000000000..46471ca4f3 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-eo/translations.xml @@ -0,0 +1,4 @@ + + + "Messages are secured with locks. Only you and the recipients can unlock them." + 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 98b0f3ee63..6de3107def 100644 --- a/features/roomdetails/impl/src/main/res/values-et/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-et/translations.xml @@ -7,7 +7,7 @@ "Küsitlused" "Vaid peakasutajad" "Suhtluskeelu seadmine" - "Sõnumite kustutamine" + "Eemalda sõnumid" "Kõik" "Kutsu teisi osalejaid ja vasta ise liitumiskutsetele" "Jututoas osalejate modereerimine" @@ -50,6 +50,8 @@ "Teavituste seadistuste laadimisel tekkis viga." "Selle jututoa summutamine ei õnnestunud. Palun proovi uuesti." "Selle jututoa summutamise eemaldamine ei õnnestunud. Palun proovi uuesti." + "Ära sulge rakendust enne, kui tegevus on lõppenud." + "Valmistan kutseid ette…" "Kutsu osalejaid" "Lahku vestlusest" "Lahku jututoast" 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 04c1411e50..e77b0ab2c6 100644 --- a/features/roomdetails/impl/src/main/res/values-fi/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fi/translations.xml @@ -21,12 +21,12 @@ "Viestien lähettäminen" "Muokkaa ylläpitäjiä" "Et voi peruuttaa tätä toimenpidettä. Ylennät käyttäjän samalle oikeustasolle kuin sinä." - "Lisää ylläpitäjä?" + "Lisätäänkö ylläpitäjä?" "Et voi kumota tätä toimintoa. Olet siirtämässä omistajuuden valituille käyttäjille. Kun poistut, muutos on pysyvä." "Siirretäänkö omistajuus?" "Alenna" "Et voi perua tätä muutosta, koska olet alentamassa itseäsi. Jos olet viimeinen oikeutettu henkilö tässä huoneessa, oikeuksia ei voi enää saada takaisin." - "Alenna itsesi?" + "Haluatko alentaa itsesi?" "%1$s (Kutsuttu)" "(Kutsuttu)" "Ylläpitäjillä on automaattisesti valvojan oikeudet" @@ -37,7 +37,7 @@ "Valvojat" "Jäsenet" "Sinulla on tallentamattomia muutoksia" - "Tallenna muutokset?" + "Tallennetaanko muutokset?" "Lisää aihe" "Salattu" "Ei salattu" @@ -50,6 +50,8 @@ "Ilmoitusasetuksia ladattaessa tapahtui virhe." "Tämän huoneen mykistäminen epäonnistui, yritä uudelleen." "Tämän huoneen mykistyksen poistaminen epäonnistui, yritä uudelleen." + "Älä sulje sovellusta ennen kuin se on valmis." + "Valmistellaan kutsuja…" "Kutsu ihmisiä" "Poistu keskustelusta" "Poistu huoneesta" 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 066ce1ee0f..c30bb2c8c9 100644 --- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml @@ -50,6 +50,8 @@ "Une erreur s’est produite lors du chargement des paramètres de notification." "Échec de la mise en sourdine de ce salon, veuillez réessayer." "Échec de la désactivation de la mise en sourdine de ce salon, veuillez réessayer." + "Ne fermez pas l’application avant que l’opération soit terminée." + "Préparation des invitations…" "Inviter des amis" "Quitter la discussion" "Quitter le 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 6e2484fc68..941389ca42 100644 --- a/features/roomdetails/impl/src/main/res/values-hu/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-hu/translations.xml @@ -50,6 +50,8 @@ "Hiba történt az értesítési beállítások betöltésekor." "Nem sikerült elnémítani ezt a szobát, próbálja újra." "Nem sikerült feloldani a szoba némítását, próbálja újra." + "Ne zárja be az alkalmazást, amíg nem végzett." + "Meghívók előkészítése…" "Ismerősök meghívása" "Beszélgetés elhagyása" "Szoba elhagyása" 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 49f44eb985..c78485bc4d 100644 --- a/features/roomdetails/impl/src/main/res/values-in/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-in/translations.xml @@ -7,7 +7,7 @@ "Pemungutan suara" "Hanya admin" "Cekal orang-orang" - "Hapus pesan" + "Hilangkan pesan" "Semua orang" "Undang orang-orang dan terima permintaan untuk bergabung" "Moderasi anggota" 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 988a2edafd..3fae2bbc04 100644 --- a/features/roomdetails/impl/src/main/res/values-it/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml @@ -22,13 +22,17 @@ "Modifica amministratori" "Non potrai annullare questa azione. Stai promuovendo l\'utente al tuo stesso livello di potere." "Aggiungi amministratore?" + "Non potrai annullare questa azione. Stai trasferendo la proprietà agli utenti selezionati. Una volta abbandonato, questa azione sarà definitiva." + "Trasferire proprietà?" "Declassa" "Non potrai annullare questa modifica perché ti stai declassando, se sei l\'ultimo utente privilegiato nella stanza, sarà impossibile riottenere i privilegi." "Declassare te stesso?" "%1$s (In attesa)" "(In attesa)" "Gli amministratori hanno automaticamente i privilegi di moderatore" + "I proprietari hanno automaticamente privilegi di amministratore." "Modifica moderatori" + "Scegli i proprietari" "Amministratori" "Moderatori" "Membri" @@ -66,7 +70,7 @@ "Aggiornamento della stanza…" "Non ci sono utenti esclusi in questa stanza." - "1 persona" + "%1$d persona" "%1$d persone" "Rimuovi ed escludi" @@ -79,6 +83,7 @@ "In attesa" "Amministratore" "Moderatore" + "Proprietario" "Membri della stanza" "Riammissione di %1$s" "Consenti impostazione personalizzata" @@ -96,12 +101,14 @@ "Solo menzioni e parole chiave" "In questa stanza, avvisami per" "Amministratori" + "Amministratori e proprietari" "Cambia il mio ruolo" "Declassa a membro" "Declassa a moderatore" "Moderazione dei membri" "Messaggi e contenuti" "Moderatori" + "Proprietari" "Autorizzazioni" "Reimpostare le autorizzazioni" "Una volta reimpostate le autorizzazioni, perderai le impostazioni correnti." diff --git a/features/roomdetails/impl/src/main/res/values-ko/translations.xml b/features/roomdetails/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..fe864a4be6 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,151 @@ + + + "디렉토리에 표시하려면 방 주소가 필요합니다." + "방 주소" + "알림 설정 업데이트 중 오류가 발생했습니다." + "귀하의 홈서버는 암호화된 방에서 이 옵션을 지원하지 않으므로, 일부 방에서는 알림이 표시되지 않을 수 있습니다." + "투표" + "관리자 전용" + "사용자 차단" + "메시지 삭제" + "모두" + "사람들을 초대하고 가입 요청을 수락합니다" + "회원 조정" + "메시지 및 콘텐츠" + "관리자 및 중재자" + "사람들을 제거하고 가입 요청을 거부합니다" + "방 아바타 변경" + "방 세부 정보" + "방 이름 변경" + "방 화제 변경" + "메시지 보내기" + "관리자 편집" + "이 작업은 실행 취소할 수 없습니다. 해당 사용자에게 당신과 동일한 권한 레벨을 부여하는 것입니다." + "관리자를 추가하시겠습니까?" + "이 작업을 취소할 수 없습니다. 선택한 사용자에게 소유권을 이전합니다. 이 작업을 완료하면 변경 사항은 영구적으로 적용됩니다." + "소유권을 이전하시겠습니까?" + "강등하다" + "이 변경 사항은 자신을 강등하는 것이므로 실행 취소할 수 없습니다. 해당 방에서 권한을 가진 마지막 사용자인 경우 권한을 다시 얻는 것은 불가능합니다." + "자신을 강등하시겠습니까?" + "%1$s (보류 중)" + "(보류 중)" + "관리자는 자동으로 중재자 권한을 갖습니다." + "소유자는 자동으로 관리자 권한을 갖습니다." + "편집 중재자" + "소유자 선택" + "관리자" + "중재자" + "회원들" + "저장되지 않은 변경 사항이 있습니다." + "변경 사항을 저장하시겠습니까?" + "화제 추가" + "암호화됨" + "암호화되지 않음" + "공개 방" + "방 편집" + "알 수 없는 오류가 발생하여 정보를 변경할 수 없습니다." + "방을 업데이트할 수 없습니다." + "메시지는 잠금으로 보호됩니다. 귀하와 수신자만 잠금을 해제할 수 있는 고유한 키를 가지고 있습니다." + "메시지 암호화 활성화됨" + "알림 설정 로딩 중 오류가 발생했습니다." + "이 방의 음소거에 실패했습니다. 다시 시도하세요." + "이 방의 음소거를 해제하지 못했습니다. 다시 시도하세요." + "사람 초대하기" + "대화에서 나가기" + "방 떠나기" + "미디어 및 파일" + "맞춤형" + "기본값" + "알림" + "고정된 메세지" + "프로필" + "참여 요청" + "역할 및 권한" + "방 이름" + "보안 및 개인정보 보호" + "보안" + "방 공유하기" + "방 정보" + "주제" + "방 업데이트 중…" + "이 방에는 차단된 사용자가 없습니다." + + "%1$d 사람" + + "방에서 차단" + "회원만 삭제할 수 있습니다." + "금지 해제" + "초대받으면 이 방에 다시 들어올 수 있습니다." + "사용자 차단 해제" + "차단됨" + "회원들" + "보류 중" + "관리자" + "중재자" + "소유자" + "방 회원들" + "차단 해제 %1$s" + "맞춤 설정 허용" + "이 기능을 활성화하면 기본 설정이 변경됩니다." + "이 채팅에서 알림 받기" + "%1$s 에서 변경할 수 있습니다." + "전역 설정" + "기본 설정" + "맞춤 설정 제거" + "알림 설정 로딩 중 오류가 발생했습니다." + "기본 모드를 복원하는 데 실패했습니다. 다시 시도하세요." + "모드 설정이 실패했습니다. 다시 시도해 주세요." + "귀하의 홈 서버는 암호화된 방에서 이 옵션을 지원하지 않으므로, 이 방에서 알림을 받지 못합니다." + "모든 메시지" + "언급 및 키워드만" + "이 방에서, 알림을 주세요" + "관리자" + "관리자 및 소유자" + "내 역할 변경" + "회원으로 강등" + "중재자로 강등시키다" + "회원 조정" + "메시지 및 콘텐츠" + "중재자" + "소유자" + "권한" + "권한 재설정" + "권한을 재설정하면 현재 설정이 모두 삭제됩니다." + "권한을 재설정하시겠습니까?" + "역할" + "방 세부 정보" + "역할 및 권한" + "방 주소 추가" + "누구나 방에 참여 요청을 할 수 있지만, 관리자나 운영자가 요청을 수락해야 합니다." + "참가 요청" + "예, 암호화 활성화" + "일단 활성화되면, 방의 암호화는 비활성화할 수 없습니다. 메시지 기록은 방에 초대된 후 또는 방에 참여한 이후부터 방 구성원만 볼 수 있습니다. +방 구성원 외에는 아무도 메시지를 읽을 수 없습니다. 이로 인해 봇과 브리지가 제대로 작동하지 않을 수 있습니다. +누구나 찾고 참여할 수 있는 방에는 암호화를 활성화하지 않는 것이 좋습니다." + "암호화 활성화?" + "일단 활성화되면, 암호화는 비활성화할 수 없습니다." + "암호화" + "종단간 암호화 활성화" + "누구나 찾을 수 있고 참여할 수 있습니다." + "누구나" + "초대받은 사용자만 가입할 수 있습니다." + "초대 전용" + "방 액세스" + "스페이스는 현재 지원되지 않습니다" + "스페이스 멤버들" + "방 디렉토리에 표시하려면 방 주소가 필요합니다." + "방 주소" + "%1$s 공개 방 디렉토리에서 이 방을 검색할 수 있도록 허용합니다" + "공개 룸 디렉토리에 표시됨" + "누구나" + "누가 기록을 읽을 수 있는가" + "초대받은 회원만 이용 가능합니다" + "이 옵션을 선택한 회원만 이용 가능합니다." + "방 주소는 방을 찾고 액세스하는 방법입니다. 이를 통해 다른 사람들과 방을 쉽게 공유할 수 있습니다. +홈서버의 공개 방 디렉토리에 방을 공개할지 여부를 선택할 수 있습니다." + "방 게시" + "방 주소는 방을 찾고 액세스하는 방법입니다. 또한 이 주소를 사용하면 다른 사람들과 방을 쉽게 공유할 수 있습니다. +%1$s 의 공개 방 디렉토리에서 방을 표시하려면 이 주소도 필요합니다." + "방 표시 여부" + "보안 및 개인정보 보호" + 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 f964f142b5..596b387b04 100644 --- a/features/roomdetails/impl/src/main/res/values-nb/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-nb/translations.xml @@ -22,13 +22,16 @@ "Rediger administratorer" "Du vil ikke kunne angre denne handlingen. Du forfremmer brukeren til å ha samme rettighetsnivå som deg." "Legg til administrator?" + "Overføre eierskapet?" "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" + "Eiere har automatisk administratorrettigheter." "Rediger moderatorer" + "Velg eiere" "Administratorer" "Moderatorer" "Medlemmer" @@ -79,6 +82,7 @@ "Venter" "Administrator" "Moderator" + "Eier" "Medlemmer av rommet" "Oppheve utestengelsen av %1$s" "Tillat egendefinert innstilling" @@ -96,12 +100,14 @@ "Bare omtaler og nøkkelord" "I dette rommet, varsle meg om" "Administratorer" + "Administratorer og eiere" "Endre rollen min" "Nedgradere til medlem" "Nedgradere til moderator" "Moderering av medlemmer" "Meldinger og innhold" "Moderatorer" + "Eiere" "Tillatelser" "Tilbakestill tillatelser" "Når du har tilbakestilt tillatelsene, mister du gjeldende innstillinger." 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 bfe0b44af2..9df66d9205 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,9 +1,9 @@ - "Você precisará de um endereço de sala para torná-lo visível no diretório." + "Você precisará de um endereço de sala para torná-la 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." + "Seu servidor-casa não suporta esta opção em salas criptografadas. Você pode não ser notificado em algumas salas." "Enquetes" "Somente administradores" "Banir pessoas" @@ -22,13 +22,17 @@ "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)" + "Você não poderá desfazer isto. Você está transferindo a posse desta sala para os usuários selecionados. Ao sair, isto será permanente." + "Transferir posse?" + "Rebaixar" + "Você não poderá desfazer essa alteração, pois estará removendo seus próprios privilégios. Se você for o último usuário privilegiado na sala, será impossível recuperar os privilégios." + "Rebaixar seu próprio privilégio?" + "%1$s (pendente)" + "(pendente)" "Os administradores têm privilégios de moderador automaticamente" + "Proprietários automaticamente têm privilégios de administradores." "Editar moderadores" + "Escolher Proprietários" "Administradores" "Moderadores" "Membros" @@ -41,11 +45,11 @@ "Editar sala" "Ocorreu um erro desconhecido e as informações não puderam ser alteradas." "Não foi possível atualizar a sala" - "As mensagens são protegidas com bloqueios. Somente você e os destinatários têm as chaves exclusivas para desbloqueá-los." + "As mensagens são protegidas com cadeados. Somente você e os destinatários têm as chaves exclusivas para desbloqueá-los." "Criptografia de mensagens ativada" "Ocorreu um erro ao carregar as configurações de notificação." "Falha ao silenciar esta sala, tente novamente." - "Falha ao ativar o som desta sala. Tente novamente." + "Falha ao desilenciar esta sala. Tente novamente." "Convidar pessoas" "Sair da conversa" "Sair da sala" @@ -55,7 +59,7 @@ "Notificações" "Mensagens fixadas" "Perfil" - "Solicitações para entrar" + "Pedidos de entrada" "Cargos e permissões" "Nome da sala" "Segurança e privacidade" @@ -69,39 +73,42 @@ "%1$d pessoa" "%1$d pessoas" - "Remover e banir membro" - "Somente remover membro" + "Banir da sala" + "Somente remover o membro" "Desbanir" - "Eles poderão entrar nesta sala novamente se forem convidados." + "Esta pessoa poderá entrar nesta sala novamente se for convidada." "Desbanir usuário" "Banidos" "Membros" "Pendente" "Administrador" "Moderador" + "Proprietário" "Membros da sala" "Desbanindo %1$s" "Permitir configuração personalizada" "Ativar isso substituirá sua configuração padrão" - "Me notifique nesta conversa para" - "Você pode alterá-lo no seu %1$s." + "Me notifique nesta conversa de" + "Você pode alterá-la nas suas %1$s." "configurações globais" "Configuração padrão" "Remover configuração personalizada" "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." + "Seu servidor-casa 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" + "Nesta sala, notifique-me de" "Administradores" + "Administradores e proprietários" "Alterar meu cargo" "Rebaixar para membro" "Rebaixar para moderador" "Moderação de membros" "Mensagens e conteúdo" "Moderadores" + "Proprietários" "Permissões" "Redefinir permissões" "Depois de redefinir as permissões, você perderá as configurações atuais." @@ -110,24 +117,24 @@ "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." + "Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou moderador terá que aceitar o pedido." "Pedir para entrar" - "Sim, habilite a criptografia" + "Sim, ativar 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?" + "Ativar a 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 um pode encontrar e entrar" "Qualquer pessoa" "As pessoas só podem participar se forem convidadas" "Somente para convidados" "Acesso à sala" - "No momento, não há compatibilidade com espaços" + "No momento, não há suporte aos espaços" "Membros do espaço" - "Você precisará de um endereço de sala para torná-lo visível no diretório de salas." + "Você precisará de um endereço de sala para torná-la 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" @@ -136,10 +143,10 @@ Não recomendamos que você ative a criptografia para salas que qualquer pessoa "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" +Você pode optar por publicar sua sala no diretório público de salas do seu servidor-casa." + "Publicação da 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." +O endereço também é necessário para que você possa ver a sala no diretório público de salas do %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 cba9ab5ac1..6338f63b0c 100644 --- a/features/roomdetails/impl/src/main/res/values-pt/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-pt/translations.xml @@ -50,6 +50,8 @@ "Erro ao carregar as configurações de notificação." "Não foi possível silenciar esta sala, por favor tenta novamente." "Não foi possível dessilenciar esta sala, por favor tenta novamente." + "Não feches a aplicação até concluir." + "A preparar convites…" "Convidar pessoas" "Sair da conversa" "Sair da sala" 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 b901be078e..fbdf3132a4 100644 --- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml @@ -1,17 +1,19 @@ + "Veți avea nevoie de o adresă de cameră pentru a o face vizibilă în director." + "Adresa camerei" "A apărut o eroare în timpul actualizării setărilor pentru notificari." "Serverul dumneavoastră nu acceptă această opțiune în camerele criptate, este posibil să nu primiți notificări în unele camere." "Sondaje" "Doar administratori" "Interziceți persoane" - "Eliminați mesaje" + "Ștergeți mesajele" "Toți" - "Invitați persoane" + "Invitați persoane și acceptați cereri de alaturare" "Moderarea membrilor" "Mesaje și conținut" "Administratori și moderatori" - "Îndepărtați persoane" + "Îndepărtați persoane și refuzați cereri de alăturare" "Schimbați avatarul camerei" "Detaliile camerei" "Schimbă numele camerei" @@ -20,13 +22,17 @@ "Editați administratorii" "Promovați utilizatorul să aibă același nivel de putere ca dumneavoastră. Nu veți putea anula această acțiune." "Adăugați administrator?" + "Nu veți putea anula această acțiune. Transferați dreptul de proprietate către utilizatorii selectați. Odată ce părăsiți această pagină, acțiunea va fi definitivă." + "Transferați proprietatea?" "Retrogradare" "Nu veți putea anula această modificare, deoarece vă retrogradați. Dacă sunteți ultimul utilizator privilegiat din cameră, va fi imposibil să recâștigați privilegiile." "Vreți să vă retrogradați?" "%1$s (În așteptare)" "(În așteptare)" "Administratorii au automat privilegii de moderator" + "Proprietarii au automat privilegii de administrator." "Editați moderatorii" + "Alegeți proprietari" "Administratori" "Moderatori" "Membri" @@ -47,12 +53,16 @@ "Invitați prieteni" "Părăsiți conversația" "Părăsiți camera" + "Media și fișiere" "Personalizat" "Implicit" "Notificări" "Mesaje fixate" + "Profil" + "Cereri de alăturare" "Roluri și permisiuni" "Numele camerei" + "Securitate & confidențialitate" "Securitate" "Partajați camera" "Informatii camera" @@ -63,7 +73,7 @@ "o persoană" "%1$d persoane" - "Eliminați și interziceți membrul" + "Îndepărtați și interziceți membrul" "Doar înlăturare" "Anulare excludere" "Se vor putea alătura din nou acestei săli dacă sunt invitați." @@ -73,6 +83,7 @@ "În așteptare" "Administrator" "Moderator" + "Proprietar" "Membrii camerei" "Se anulează interzicerea lui %1$s" "Permiteți setări personalizate" @@ -81,7 +92,7 @@ "Îl puteți schimba în %1$s." "Setări generale" "Setare implicită" - "Stergeți setarea personalizată" + "Ștergeți setarea personalizată" "A apărut o eroare la încărcarea setărilor pentry notificari." "Nu s-a reușit restaurarea modului implicit, vă rugăm să încercați din nou." "Nu s-a reușit setarea modului, vă rugăm să încercați din nou." @@ -90,12 +101,14 @@ "Numai mențiuni și cuvinte cheie" "În această cameră, anunțați-mă pentru" "Administratori" + "Administratori și proprietari" "Schimbare rol" "Degradare la membru" "Degradare la moderator" "Moderarea membrilor" "Mesaje și conținut" "Moderatori" + "Proprietari" "Permisiuni" "Resetați permisiunile" "După ce resetați permisiunile, veți pierde setările curente." @@ -103,8 +116,37 @@ "Roluri" "Detaliile camerei" "Roluri și permisiuni" + "Adăugați adresa camerei" + "Oricine poate cere să se alăture camerei, dar un administrator sau moderator va trebui să accepte cererea." "Cereți să vă alăturați" + "Da, activați criptarea" + "Odată activată, criptarea pentru o cameră nu poate fi dezactivată. Mesajele anterioare vor fi vizibile numai pentru membrii camerei de la momentul la care au fost invitați sau de la momentul la care s-au alăturat camerei. +Nimeni în afară de membrii camerei nu va putea citi messaje. Acest lucru poate împiedica funcționarea corectă a boților și a punților. +Nu recomandăm activarea criptării pentru camerele pe care oricine le poate găsi și la care se poate alătura." + "Activați criptarea?" + "Odată activată, criptarea nu poate fi dezactivată." "Criptare" + "Activați criptarea end-to-end" + "Oricine poate găsi și alătura camerei" "Oricine" + "Persoanele se pot alătura numai dacă invitate" + "Doar pe bază de invitație" + "Acces la cameră" + "Spațiile nu sunt momentan suportate." + "Membrii spațiului" + "Veți avea nevoie de o adresă de cameră pentru a o face vizibilă în directorul de camere." + "Adresa camerei" + "Permiteți găsirea acestei camere prin căutarea în directorul de camere publice al %1$s" + "Vizibilă în directorul de camere publice" "Oricine" + "Cine poate citi mesajele anterioare" + "Doar pentru membri, de la momentul în care au fost invitați" + "Doar pentru membri, după selectarea acestei opțiuni" + "Adresele camerelor sunt modalități de a găsi și accesa camere. Acest lucru vă asigură, de asemenea, că puteți partaja cu ușurință camera dumneavoastră cu alte persoane. +Puteți alege să publicați camera în directorul public al camerelor serverului dumneavoastră." + "Publicare cameră" + "Adresele camerelor sunt modalități de a găsi și accesa camerele. Acest lucru vă asigură, de asemenea, că puteți partaja cu ușurință camera dvs. cu alte persoane. +Adresa este necesară și pentru ca camera să fie vizibilă în directorul public de camere al %1$s." + "Vizibilitatea camerei" + "Securitate & confidențialitate" 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 e4c53ff30d..e24adbd6c1 100644 --- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml @@ -7,13 +7,13 @@ "Опросы" "Только администраторы" "Блокировать людей могут" - "Удалять сообщения могут" + "Удалить сообщения" "Все" - "Приглашайте людей и принимайте заявки на присоединение" + "Приглашать людей и принимать запросы на присоединение могут" "Модерация участников" "Сообщения и содержание" "Администраторы и модераторы" - "Удаляйте пользователей и отклоняйте запросы на присоединение" + "Удалять людей и отклонять запросы на присоединение могут" "Менять изображение комнаты могут" "Информация о комнате" "Менять название комнаты могут" @@ -22,13 +22,17 @@ "Редактировать роль администраторов" "Вы не сможете отменить это действие. Вы устанавливаете уровень пользователю соответствующий вашему." "Добавить администратора?" + "Отменить данное действие будет невозможно. Владение передастся выбранным пользователям. После вашего выхода действие станет необратимым." + "Передать владение?" "Понизить уровень" "Вы не сможете отменить это изменение, так как понижаете себя статус. Если вы являетесь последним привилегированным пользователем в комнате, восстановить привилегии будет невозможно." "Понизить свой уровень?" "%1$s (Ожидание)" "(В ожидании)" "Администраторы автоматически получают права модератора" + "Владельцы автоматически получают права администратора." "Редактировать роль модераторов" + "Назначить владельцев" "Администраторы" "Модераторы" "Участники" @@ -80,6 +84,7 @@ "В ожидании" "Администратор" "Модератор" + "Владелец" "Участники комнаты" "Разблокировка %1$s" "Разрешить пользовательские настройки" @@ -97,12 +102,14 @@ "Только упоминания и ключевые слова" "В этой комнате уведомлять меня" "Администраторы" + "Администраторы и владельцы" "Изменить мою роль" "Понизить до участника" "Понизить до модератора" "Модерация участников" "Сообщения и содержание" "Модераторы" + "Владельцы" "Разрешения" "Сбросить разрешения" "Как только вы сбросите разрешения, все текущие настройки будут утеряны." 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 a3a9d4f144..1cc11933ab 100644 --- a/features/roomdetails/impl/src/main/res/values-sv/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sv/translations.xml @@ -22,13 +22,17 @@ "Redigera administratörer" "Du kommer inte att kunna ångra den här åtgärden. Du befordrar användaren till att ha samma behörighetsnivå som du." "Lägg till Admin?" + "Du kommer inte att kunna ångra den här åtgärden. Du överför ägarskapet till de valda användarna. När du lämnar kommer detta att vara permanent." + "Överför ägarskap?" "Degradera" "Du kommer inte att kunna ångra denna ändring eftersom du degraderar dig själv, om du är den sista privilegierade användaren i rummet kommer det att vara omöjligt att återfå privilegier." "Degradera dig själv?" "%1$s (Väntar)" "(Väntar)" "Administratörer har automatiskt moderatorbehörighet" + "Ägare har automatiskt administratörsbehörighet." "Redigera moderatorer" + "Välj ägare" "Administratörer" "Moderatorer" "Medlemmar" @@ -79,6 +83,7 @@ "Väntar" "Admin" "Moderator" + "Ägare" "Rumsmedlemmar" "Avbannar %1$s" "Tillåt anpassad inställning" @@ -96,12 +101,14 @@ "Endast omnämnanden och nyckelord" "I det här rummet, meddela mig för" "Administratörer" + "Administratörer och ägare" "Ändra min roll" "Degradera till medlem" "Degradera till moderator" "Medlemsmoderering" "Meddelanden och innehåll" "Moderatorer" + "Ägare" "Behörigheter" "Återställ behörigheter" "När du har återställt behörigheterna kommer du att förlora de aktuella inställningarna." diff --git a/features/roomdetails/impl/src/main/res/values-uz/translations.xml b/features/roomdetails/impl/src/main/res/values-uz/translations.xml index 229b201926..9ced40ed1f 100644 --- a/features/roomdetails/impl/src/main/res/values-uz/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-uz/translations.xml @@ -1,8 +1,41 @@ "Bildirishnoma sozlamalarini yangilashda xatolik yuz berdi." + "Uy serveringiz shifrlangan xonalarda ushbu imkoniyatni qoʻllab-quvvatlamaydi, shuning uchun baʼzi xonalardagi xabarlarni olmasligingiz mumkin." + "Soʻrovnomalar" + "Faqat adminlar" + "Odamlarni taqiqlash" + "Xabarlarni olib tashlash" "Har kim" + "Odamlarni taklif qiling va qo‘shilish so‘rovlarini qabul qiling" + "Aʻzo moderatsiyasi" + "Xabarlar va kontent" + "Adminlar va moderatorlar" + "Odamlarni olib tashlash va qoʻshilish soʻrovlarini rad etish" + "Xona avatarini oʻzgartirish" + "Xona tafsilotlari" + "Xona nomini oʻzgartirish" + "Xona mavzusini almashtirish" + "Xabarlar yuborish" + "Administratorlarni tahrirlash" + "Bu amalni bekor qila olmaysiz. Siz foydalanuvchini o‘zingiz bilan bir xil quvvat darajasiga ega bo‘lishga undayapsiz." + "Admin qo‘shilsinmi?" + "Pastga tushirish" + "Siz oʻzingizni imtiyozlardan mahrum qilayotganingiz sababli, bu o‘zgarishni bekor qila olmaysiz. Agar xonadagi so‘nggi imtiyozli foydalanuvchi bo‘lsangiz, imtiyozlarni qayta tiklash imkonsiz bo‘ladi." + "O‘z darajangizni pasaytirmoqchimisiz?" + "%1$s (Jarayonda)" + "(Kutilmoqda)" + "Administratorlar avtomatik ravishda moderator imtiyozlariga ega" + "Moderatorlarni tahrirlash" + "Adminlar" + "Moderatorlar" + "Azolar" + "Sizda saqlanmagan oʻzgarishlar bor" + "O‘zgartirishlarni saqlaysizmi?" "Mavzu qo\'shish" + "Shifrlangan" + "Shifrlanmagan" + "Jamoat xonasi" "Xonani tahrirlash" "Nomaʼlum xatolik yuz berdi va maʼlumotni oʻzgartirib boʻlmadi." "Xonani yangilab bo‘lmadi" @@ -17,17 +50,31 @@ "Maxsus" "Standart" "Bildirishnomalar" + "Qadalgan xabarlar" + "Rollar va ruxsatlar" "Xona nomi" "Xavfsizlik" "Xonani baham ko\'ring" + "Xona haqida maʼlumot" "Mavzu" "Xona yangilanmoqda…" + "Bu xonada taqiqlangan foydalanuvchilar yoʻq." "%1$dodam" "%1$dodamlar" + "Xonadan chetlashtirish" + "Faqat aʻzoni olib tashlash" + "Taqiqni bekor qilish" + "Agar taklif qilinsa, ular bu xonaga qayta qo‘shilishlari mumkin." + "Foydalanuvchini blokdan chiqarish" + "Taqiqlangan" + "Azolar" "Kutilmoqda" + "Admin" + "Moderator" "Xona a\'zolari" + "Taqiqni bekor qilish %1$s" "Moslashtirilgan sozlamalarga ruxsat bering" "Buni yoqsangiz, standart sozlamalaringiz bekor qilinadi" "Bu chatda menga xabar bering" @@ -38,8 +85,27 @@ "Bildirishnoma sozlamalarini yuklashda xatolik yuz berdi." "Standart rejimni tiklab bo‘lmadi, qaytadan urinib ko‘ring." "Rejimni o‘rnatib bo‘lmadi, qayta urinib ko‘ring." + "Uy serveringiz shifrlangan xonalarda ushbu imkoniyatni qoʻllab-quvvatlamaydi, shuning uchun bu xonadan bildirishnomalar olmaysiz." "Barcha xabarlar" "Faqat eslatmalar va kalit so\'zlar" "Bu xonada menga xabar bering" + "Adminlar" + "Rolimni o‘zgartirish" + "Aʼzolikka tushirish" + "Moderatorga pasaytirish" + "Aʻzo moderatsiyasi" + "Xabarlar va kontent" + "Moderatorlar" + "Ruxsatlar" + "Ruxsatlarni tiklash" + "Ruxsatlarni asliga qaytargach, joriy sozlamalarni yoʻqotasiz." + "Ruxsatlar asliga qaytarilsinmi?" + "Rollar" + "Xona tafsilotlari" + "Rollar va ruxsatlar" + "Qo‘shilishni so‘rang" "Shifrlash" + "Har kim" + "Har kim" + "Xonaning ko‘rinishi" 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 d2d6d0ef90..035de6fa4d 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 @@ -50,6 +50,8 @@ "載入通知設定時發生錯誤。" "無法關閉聊天室通知,請再試一次。" "無法開啟聊天室通知,請再試一次。" + "完成前請勿關閉應用程式。" + "正在準備邀請……" "邀請夥伴" "離開對話" "離開聊天室" diff --git a/features/roomdetails/impl/src/main/res/values-zh/translations.xml b/features/roomdetails/impl/src/main/res/values-zh/translations.xml index 7d5a9e330c..48a92f93a2 100644 --- a/features/roomdetails/impl/src/main/res/values-zh/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-zh/translations.xml @@ -22,13 +22,17 @@ "编辑管理员" "您将无法撤消此操作。您正在提升用户的权限,使其拥有与您平权。" "添加管理员?" + "此操作无法撤销。您正在将所有权转移给所选用户。一旦离开此界面,该操作将永久生效。" + "转让所有权" "降级" "您正在降级,此更改将无法撤消。如果您是聊天室中的最后一个特权用户,则无法重新获得权限。" "降级自己?" "%1$s(待处理)" "(已邀请)" "管理员自动拥有协管员权限" + "所有者自动拥有管理员权限。" "编辑协管员" + "选择所有者" "管理员" "协管员" "成员" @@ -78,6 +82,7 @@ "待处理" "管理员" "协管员" + "所有者" "聊天室成员" "解除封禁 %1$s" "允许自定义设置" @@ -95,12 +100,14 @@ "仅限提及和关键词" "在这个聊天室,通知我:" "管理员" + "管理员和所有者" "更改我的角色" "降级为成员" "降级为协管员" "成员权限" "消息和内容" "协管员" + "所有者" "权限" "重置权限" "重置权限后,您将丢失当前设置。" diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index 5dd185ec13..3f4541dacb 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -50,6 +50,8 @@ "An error occurred when loading notification settings." "Failed muting this room, please try again." "Failed unmuting this room, please try again." + "Don\'t close the app until finished." + "Preparing invitations…" "Invite people" "Leave conversation" "Leave room" diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt new file mode 100644 index 0000000000..f2412e616b --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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 androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.ElementCallEntryPoint +import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint +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.verifysession.api.OutgoingVerificationEntryPoint +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.test.A_USER_ID +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint +import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultRoomDetailsEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultRoomDetailsEntryPoint() + + val parentNode = TestParentNode.create { buildContext, plugins -> + RoomDetailsFlowNode( + buildContext = buildContext, + plugins = plugins, + pollHistoryEntryPoint = object : PollHistoryEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + elementCallEntryPoint = object : ElementCallEntryPoint { + override fun startCall(callType: CallType) = lambdaError() + override suspend fun handleIncomingCall( + callType: CallType.RoomCall, + eventId: EventId, + senderId: UserId, + roomName: String?, + senderName: String?, + avatarUrl: String?, + timestamp: Long, + expirationTimestamp: Long, + notificationChannelId: String, + textContent: String? + ) = lambdaError() + }, + room = FakeJoinedRoom(), + analyticsService = FakeAnalyticsService(), + messagesEntryPoint = object : MessagesEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + knockRequestsListEntryPoint = object : KnockRequestsListEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + mediaViewerEntryPoint = object : MediaViewerEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + mediaGalleryEntryPoint = object : MediaGalleryEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + outgoingVerificationEntryPoint = object : OutgoingVerificationEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + reportRoomEntryPoint = object : ReportRoomEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext, roomId: RoomId) = lambdaError() + }, + changeRoomMemberRolesEntryPoint = object : ChangeRoomMemberRolesEntryPoint { + override fun builder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + ) + } + val callback = object : RoomDetailsEntryPoint.Callback { + override fun onOpenGlobalNotificationSettings() = lambdaError() + override fun onOpenRoom(roomId: RoomId, serverNames: List) = lambdaError() + override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() + override fun onForwardedToSingleRoom(roomId: RoomId) = lambdaError() + } + val params = RoomDetailsEntryPoint.Params( + initialElement = RoomDetailsEntryPoint.InitialTarget.RoomDetails, + ) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .callback(callback) + .build() + assertThat(result).isInstanceOf(RoomDetailsFlowNode::class.java) + assertThat(result.plugins).contains(params) + assertThat(result.plugins).contains(callback) + } + + @Test + fun `test initial target to nav target mapping`() { + assertThat(RoomDetailsEntryPoint.InitialTarget.RoomDetails.toNavTarget()) + .isEqualTo(RoomDetailsFlowNode.NavTarget.RoomDetails) + assertThat(RoomDetailsEntryPoint.InitialTarget.RoomMemberDetails(A_USER_ID).toNavTarget()) + .isEqualTo(RoomDetailsFlowNode.NavTarget.RoomMemberDetails(A_USER_ID)) + assertThat(RoomDetailsEntryPoint.InitialTarget.RoomNotificationSettings.toNavTarget()) + .isEqualTo(RoomDetailsFlowNode.NavTarget.RoomNotificationSettings(showUserDefinedSettingStyle = true)) + } +} 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 index 0491c5d437..03555fb967 100644 --- 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 @@ -116,9 +116,6 @@ class RoomMemberListPresenterTest { } } - // Wait for the update to be processed - skipItems(1) - // Update the room members state as `Room.updateMembers()` would have done with the actual implementation room.givenRoomMembersState(RoomMembersState.Ready(persistentListOf())) // Wait for another update diff --git a/features/roomdirectory/impl/build.gradle.kts b/features/roomdirectory/impl/build.gradle.kts index 3f48136514..ec858ced09 100644 --- a/features/roomdirectory/impl/build.gradle.kts +++ b/features/roomdirectory/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2024 New Vector Ltd. @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.roomdirectory.api) @@ -35,14 +36,6 @@ dependencies { implementation(projects.libraries.testtags) implementation(projects.services.analytics.api) - testImplementation(libs.test.junit) - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) - testImplementation(libs.test.robolectric) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.tests.testutils) } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt index f3963cb0dd..135b2d5aea 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt @@ -10,15 +10,16 @@ package io.element.android.features.roomdirectory.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.features.roomdirectory.impl.root.RoomDirectoryNode import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultRoomDirectoryEntryPoint @Inject constructor() : RoomDirectoryEntryPoint { +@Inject +class DefaultRoomDirectoryEntryPoint : RoomDirectoryEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDirectoryEntryPoint.NodeBuilder { val plugins = ArrayList() diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt index d4b6026833..03d2be6e35 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt @@ -13,15 +13,16 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class RoomDirectoryNode @AssistedInject constructor( +@AssistedInject +class RoomDirectoryNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: RoomDirectoryPresenter, diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt index 969eaa8352..4696dd4263 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryListState import io.element.android.features.roomdirectory.impl.root.model.toFeatureModel import io.element.android.libraries.architecture.Presenter @@ -26,11 +27,11 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import javax.inject.Inject private const val SEARCH_BATCH_SIZE = 20 -class RoomDirectoryPresenter @Inject constructor( +@Inject +class RoomDirectoryPresenter( private val dispatchers: CoroutineDispatchers, private val roomDirectoryService: RoomDirectoryService, ) : Presenter { diff --git a/features/roomdirectory/impl/src/main/res/values-de/translations.xml b/features/roomdirectory/impl/src/main/res/values-de/translations.xml index ea81cdb862..d62411a821 100644 --- a/features/roomdirectory/impl/src/main/res/values-de/translations.xml +++ b/features/roomdirectory/impl/src/main/res/values-de/translations.xml @@ -1,5 +1,5 @@ "Fehler beim Laden" - "Raum-Verzeichnis" + "Chat-Verzeichnis" diff --git a/features/roomdirectory/impl/src/main/res/values-ko/translations.xml b/features/roomdirectory/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..d16886307c --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,5 @@ + + + "로드에 실패했습니다" + "방 디렉토리" + diff --git a/features/roomdirectory/impl/src/main/res/values-uz/translations.xml b/features/roomdirectory/impl/src/main/res/values-uz/translations.xml new file mode 100644 index 0000000000..9f4cbe607d --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-uz/translations.xml @@ -0,0 +1,5 @@ + + + "Yuklab bo‘lmadi" + "Xona katalogi" + diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPointTest.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPointTest.kt new file mode 100644 index 0000000000..d544f55000 --- /dev/null +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPointTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roomdirectory.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomdirectory.api.RoomDescription +import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint +import io.element.android.features.roomdirectory.impl.root.RoomDirectoryNode +import io.element.android.features.roomdirectory.impl.root.createRoomDirectoryPresenter +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class DefaultRoomDirectoryEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() = runTest { + val entryPoint = DefaultRoomDirectoryEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + RoomDirectoryNode( + buildContext = buildContext, + plugins = plugins, + presenter = createRoomDirectoryPresenter(), + ) + } + val callback = object : RoomDirectoryEntryPoint.Callback { + override fun onResultClick(roomDescription: RoomDescription) = lambdaError() + } + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .callback(callback) + .build() + assertThat(result).isInstanceOf(RoomDirectoryNode::class.java) + assertThat(result.plugins).contains(callback) + } +} 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 d6ebb6cd95..4af983b307 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 @@ -122,15 +122,15 @@ class RoomDirectoryPresenterTest { .isCalledOnce() .withNoParameter() } - - private fun TestScope.createRoomDirectoryPresenter( - roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService( - createRoomDirectoryListFactory = { FakeRoomDirectoryList() } - ), - ): RoomDirectoryPresenter { - return RoomDirectoryPresenter( - dispatchers = testCoroutineDispatchers(), - roomDirectoryService = roomDirectoryService, - ) - } +} + +internal fun TestScope.createRoomDirectoryPresenter( + roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService( + createRoomDirectoryListFactory = { FakeRoomDirectoryList() } + ), +): RoomDirectoryPresenter { + return RoomDirectoryPresenter( + dispatchers = testCoroutineDispatchers(), + roomDirectoryService = roomDirectoryService, + ) } diff --git a/features/roommembermoderation/impl/build.gradle.kts b/features/roommembermoderation/impl/build.gradle.kts index 93294988f6..a58b0ce02f 100644 --- a/features/roommembermoderation/impl/build.gradle.kts +++ b/features/roommembermoderation/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2025 New Vector Ltd. @@ -20,7 +21,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.core) @@ -32,16 +33,8 @@ dependencies { 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) + testCommonDependencies(libs, true) 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 index 681a1eb733..830e3ef984 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt @@ -10,17 +10,18 @@ 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 dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 { +@Inject +class DefaultRoomMemberModerationRenderer : RoomMemberModerationRenderer { @Composable override fun Render( state: RoomMemberModerationState, 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 index f0861a735e..3d07ee75e1 100644 --- 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 @@ -15,6 +15,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.RoomModeration import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.ModerationActionState @@ -38,12 +39,14 @@ import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds -class RoomMemberModerationPresenter @Inject constructor( +@Inject +class RoomMemberModerationPresenter( private val room: JoinedRoom, private val dispatchers: CoroutineDispatchers, private val analyticsService: AnalyticsService, @@ -82,6 +85,9 @@ class RoomMemberModerationPresenter @Inject constructor( ) } is RoomMemberModerationEvents.ProcessAction -> { + // First, hide any list of existing actions that could be displayed + moderationActions.value = persistentListOf() + when (event.action) { is ModerationAction.DisplayProfile -> Unit is ModerationAction.KickUser -> { @@ -118,6 +124,7 @@ class RoomMemberModerationPresenter @Inject constructor( } is InternalRoomMemberModerationEvents.Reset -> { selectedUser = null + moderationActions.value = persistentListOf() kickUserAsyncAction.value = AsyncAction.Uninitialized banUserAsyncAction.value = AsyncAction.Uninitialized unbanUserAsyncAction.value = AsyncAction.Uninitialized @@ -204,7 +211,15 @@ class RoomMemberModerationPresenter @Inject constructor( action.runUpdatingState { val result = block() if (result.isSuccess) { - room.membersStateFlow.drop(1).take(1) + // We wait a bit to ensure the server has processed the membership change + delay(50.milliseconds) + + // Update the members to ensure we have the latest state + launch { room.updateMembers() } + + // Wait for the membership change to be processed and returned + // We drop the first emission as it's the current state + room.membersStateFlow.drop(1).first() } 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 index d4b7a7b69e..248b6cd02b 100644 --- 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 @@ -242,7 +242,7 @@ private fun RoomMemberActionsBottomSheet( ) } Text( - text = user.userId.toString(), + text = user.userId.value, style = ElementTheme.typography.fontBodyLgRegular, color = ElementTheme.colors.textSecondary, maxLines = 1, 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 index d2a5296b95..9e4592d475 100644 --- 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 @@ -7,16 +7,16 @@ package io.element.android.features.roommembermoderation.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo 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 +@BindingContainer interface RoomMemberModerationModule { @Binds fun bindRoomMemberModerationPresenter(presenter: RoomMemberModerationPresenter): Presenter diff --git a/features/roommembermoderation/impl/src/main/res/values-de/translations.xml b/features/roommembermoderation/impl/src/main/res/values-de/translations.xml index 3ded15cd8e..d977d05564 100644 --- a/features/roommembermoderation/impl/src/main/res/values-de/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-de/translations.xml @@ -2,19 +2,19 @@ "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?" + "Sie können diesem Chat auch auf Einladung nicht erneut beitreten." + "Möchtest du 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?" + "Sie können diesem Chat auf Einladung wieder beitreten." + "Möchtest du 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 für diesen Chat 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?" + "Sie können dann diesem Chat auf Einladung wieder beitreten." + "Möchtest du die Sperre dieses Mitglieds wirklich aufheben?" "Sperre für %1$s aufheben" diff --git a/features/roommembermoderation/impl/src/main/res/values-ko/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..4831c177cc --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,20 @@ + + + "방에서 차단" + "차단" + "초대하더라도 그들은 이 방에 다시 참여할 수 없습니다." + "정말로 이 회원을 차단하시겠습니까?" + "차단 %1$s" + "제거" + "초대되면 이 방에 다시 참여할 수 있습니다." + "이 회원을 정말로 제거하시겠습니까?" + "프로필 보기" + "방에서 제거" + "회원을 삭제하고 앞으로 가입을 금지하시겠습니까?" + "%1$s 제거 중…" + "방에서 차단 해제" + "차단 해제" + "초대되면 다시 방에 참여할 수 있습니다." + "이 회원을 정말로 차단해제 하시겠습니까?" + "차단 해제 %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 index 6d6f24ef8d..803eda1f80 100644 --- a/features/roommembermoderation/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-pt-rBR/translations.xml @@ -1,12 +1,12 @@ - "Remover e banir membro" + "Banir da sala" "Banir" - "Eles não poderão entrar nesta sala novamente se forem convidados." + "Essa pessoa não poderá entrar nesta sala novamente se for convidada." "Tem certeza de que quer banir este membro?" "Banindo %1$s" "Remover" - "Eles poderão entrar nesta sala novamente se forem convidados." + "Essa pessoa poderá entrar na sala novamente se for convidada." "Tem certeza de que deseja remover este membro?" "Ver perfil" "Remover da sala" diff --git a/features/roommembermoderation/impl/src/main/res/values-pt/translations.xml b/features/roommembermoderation/impl/src/main/res/values-pt/translations.xml index 89a0992c98..42ceede746 100644 --- a/features/roommembermoderation/impl/src/main/res/values-pt/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-pt/translations.xml @@ -12,9 +12,9 @@ "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" + "Desbanir da sala" + "Desbanir" + "Eles poderão entrar novamente na sala se forem convidados" + "Tens certeza que queres desbanir este membro?" + "Desbanindo %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 index 27bb9c23bd..fa05933234 100644 --- a/features/roommembermoderation/impl/src/main/res/values-ro/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-ro/translations.xml @@ -1,12 +1,20 @@ - "Eliminați și interziceți membrul" + "Îndepărtaț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" + "Îndepărtați" + "Se vor putea alătura din nou acestei camere dacă sunt invitați." + "Sunteți sigur că doriți să îndepărtați acest membru?" "Vizualizare profil" "Înlăturați membrul" "Înlăturați membrul și interziceți-i să se alăture în viitor?" - "Se elimină %1$s" + "Se îndepărtează %1$s" + "Revocati excluderea din camera" + "Anulați excluderea" + "Aceștia se vor putea alătura din nou camerei dacă sunt invitați." + "Sunteți sigur că doriți să dezactivați excluderea impusă acestui membru?" + "Se anulează excluderea lui %1$s" diff --git a/features/roommembermoderation/impl/src/main/res/values-uz/translations.xml b/features/roommembermoderation/impl/src/main/res/values-uz/translations.xml new file mode 100644 index 0000000000..2b08fe58a0 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-uz/translations.xml @@ -0,0 +1,12 @@ + + + "Xonadan chetlashtirish" + "Taqiqlash" + "Taklif qilingan taqdirda ham, ular bu xonaga boshqa qo‘shila olmaydilar." + "Haqiqatan ham bu aʼzoni taqiqlamoqchimisiz?" + "Taqiqlash %1$s" + "Profilni koʻrish" + "Xonadan olib tashlash" + "Aʻzo oʻchirilsinmi va kelgusida qoʻshilish taqiqlansinmi?" + "Oʻchirish %1$s …" + diff --git a/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml index 3eb55ef12b..54a0978da1 100644 --- a/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml @@ -12,7 +12,9 @@ "从聊天室移除" "删除成员并禁止重新加入?" "正在移除 %1$s……" + "从房间取消解封" "解除封禁" "如果再次收到邀请,他们可以重新加入该聊天室" "确定要解除该成员的封禁吗?" + "解除封禁 %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 index 2d1bf77fe0..3b647c6478 100644 --- 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 @@ -29,6 +29,7 @@ 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.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -217,7 +218,14 @@ class RoomMemberModerationPresenterTest { @Test fun `present - do kick user with success`() = runTest { - createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val room = aJoinedRoom() + room.baseRoom.givenUpdateMembersResult { + // Simulate the member list being updated + room.givenRoomMembersState(RoomMembersState.Ready( + persistentListOf(aRoomMember()) + )) + } + createRoomMemberModerationPresenter(room = room).test { val initialState = awaitState() initialState.eventSink( RoomMemberModerationEvents.ProcessAction( @@ -238,7 +246,14 @@ class RoomMemberModerationPresenterTest { @Test fun `present - do ban user with success`() = runTest { - createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val room = aJoinedRoom() + room.baseRoom.givenUpdateMembersResult { + // Simulate the member list being updated + room.givenRoomMembersState(RoomMembersState.Ready( + persistentListOf(aRoomMember()) + )) + } + createRoomMemberModerationPresenter(room = room).test { val initialState = awaitState() initialState.eventSink( RoomMemberModerationEvents.ProcessAction( @@ -259,7 +274,14 @@ class RoomMemberModerationPresenterTest { @Test fun `present - do unban user with success`() = runTest { - createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val room = aJoinedRoom() + room.baseRoom.givenUpdateMembersResult { + // Simulate the member list being updated + room.givenRoomMembersState(RoomMembersState.Ready( + persistentListOf(aRoomMember()) + )) + } + createRoomMemberModerationPresenter(room = room).test { val initialState = awaitState() initialState.eventSink( RoomMemberModerationEvents.ProcessAction( @@ -326,7 +348,7 @@ class RoomMemberModerationPresenterTest { banUserResult: Result = Result.success(Unit), unBanUserResult: Result = Result.success(Unit), targetRoomMember: RoomMember? = null, - ): JoinedRoom { + ): FakeJoinedRoom { return FakeJoinedRoom( kickUserResult = { _, _ -> kickUserResult }, banUserResult = { _, _ -> banUserResult }, @@ -335,6 +357,7 @@ class RoomMemberModerationPresenterTest { canBanResult = { _ -> Result.success(canBan) }, canKickResult = { _ -> Result.success(canKick) }, userRoleResult = { Result.success(myUserRole) }, + updateMembersResult = { Result.success(Unit) } ), ).apply { val roomMembers = listOfNotNull(targetRoomMember).toPersistentList() diff --git a/features/securebackup/impl/build.gradle.kts b/features/securebackup/impl/build.gradle.kts index 68916b4fe8..92a6013580 100644 --- a/features/securebackup/impl/build.gradle.kts +++ b/features/securebackup/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -22,7 +23,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.appconfig) @@ -38,14 +39,6 @@ dependencies { api(libs.statemachine) api(projects.features.securebackup.api) - 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) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.tests.testutils) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt index 7d68c09496..6d5358299b 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt @@ -10,14 +10,15 @@ package io.element.android.features.securebackup.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultSecureBackupEntryPoint @Inject constructor() : SecureBackupEntryPoint { +@Inject +class DefaultSecureBackupEntryPoint : SecureBackupEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): SecureBackupEntryPoint.NodeBuilder { val plugins = ArrayList() diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt index 92e2899173..4d79f8ea1b 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt @@ -17,9 +17,9 @@ import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode @@ -34,7 +34,8 @@ import io.element.android.libraries.di.SessionScope import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -class SecureBackupFlowNode @AssistedInject constructor( +@AssistedInject +class SecureBackupFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, ) : BaseFlowNode( diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableNode.kt index 5d76d6ee08..8af4f3e613 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableNode.kt @@ -12,13 +12,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class SecureBackupDisableNode @AssistedInject constructor( +@AssistedInject +class SecureBackupDisableNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: SecureBackupDisablePresenter, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt index 0acf867095..aa1de8fde9 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import dev.zacsweers.metro.Inject import io.element.android.features.securebackup.impl.loggerTagDisable import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter @@ -23,9 +24,9 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject -class SecureBackupDisablePresenter @Inject constructor( +@Inject +class SecureBackupDisablePresenter( private val encryptionService: EncryptionService, private val buildMeta: BuildMeta, ) : Presenter { diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyNode.kt index e8d31780bb..77d1fe8f32 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyNode.kt @@ -13,13 +13,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class SecureBackupEnterRecoveryKeyNode @AssistedInject constructor( +@AssistedInject +class SecureBackupEnterRecoveryKeyNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: SecureBackupEnterRecoveryKeyPresenter, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenter.kt index 313f1526af..b56a4542b0 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenter.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenter.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState import io.element.android.features.securebackup.impl.tools.RecoveryKeyTools @@ -24,9 +25,9 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.encryption.EncryptionService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import javax.inject.Inject -class SecureBackupEnterRecoveryKeyPresenter @Inject constructor( +@Inject +class SecureBackupEnterRecoveryKeyPresenter( private val encryptionService: EncryptionService, private val recoveryKeyTools: RecoveryKeyTools, ) : Presenter { diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt index a0827e4061..fe6a86a357 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt @@ -7,6 +7,7 @@ package io.element.android.features.securebackup.impl.reset +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient @@ -20,9 +21,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import javax.inject.Inject -class ResetIdentityFlowManager @Inject constructor( +@Inject +class ResetIdentityFlowManager( private val matrixClient: MatrixClient, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val sessionVerificationService: SessionVerificationService, 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 6ce66d149d..dfc9425ebe 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 @@ -23,9 +23,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.features.securebackup.impl.reset.password.ResetIdentityPasswordNode import io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootNode @@ -47,7 +47,8 @@ import kotlinx.parcelize.Parcelize import timber.log.Timber @ContributesNode(SessionScope::class) -class ResetIdentityFlowNode @AssistedInject constructor( +@AssistedInject +class ResetIdentityFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val resetIdentityFlowManager: ResetIdentityFlowManager, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt index dd1463314d..3c22673bff 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt @@ -12,9 +12,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -22,7 +22,8 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle @ContributesNode(SessionScope::class) -class ResetIdentityPasswordNode @AssistedInject constructor( +@AssistedInject +class ResetIdentityPasswordNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, coroutineDispatchers: CoroutineDispatchers, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt index 626edc9c67..8267242f97 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt @@ -12,13 +12,14 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class ResetIdentityRootNode @AssistedInject constructor( +@AssistedInject +class ResetIdentityRootNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, ) : Node(buildContext, plugins = plugins) { diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt index 5d4196b485..6d4db197d3 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt @@ -15,14 +15,15 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.appconfig.LearnMoreConfig import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class SecureBackupRootNode @AssistedInject constructor( +@AssistedInject +class SecureBackupRootNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: SecureBackupRootPresenter, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt index 4b9d768fcc..67ec1c9960 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject import io.element.android.features.securebackup.impl.loggerTagDisable import io.element.android.features.securebackup.impl.loggerTagRoot import io.element.android.libraries.architecture.AsyncAction @@ -30,9 +31,9 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject -class SecureBackupRootPresenter @Inject constructor( +@Inject +class SecureBackupRootPresenter( private val encryptionService: EncryptionService, private val buildMeta: BuildMeta, private val snackbarDispatcher: SnackbarDispatcher, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupNode.kt index 03f4424158..6adcb890ae 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupNode.kt @@ -12,9 +12,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.securebackup.impl.R import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs @@ -23,7 +23,8 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class SecureBackupSetupNode @AssistedInject constructor( +@AssistedInject +class SecureBackupSetupNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: SecureBackupSetupPresenter.Factory, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt index eb84ac5341..58a6c4b43c 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt @@ -18,9 +18,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import com.freeletics.flowredux.compose.StateAndDispatch import com.freeletics.flowredux.compose.rememberStateAndDispatch -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.securebackup.impl.loggerTagSetup import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState @@ -32,7 +32,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import timber.log.Timber -class SecureBackupSetupPresenter @AssistedInject constructor( +@AssistedInject +class SecureBackupSetupPresenter( @Assisted private val isChangeRecoveryKeyUserStory: Boolean, private val stateMachine: SecureBackupSetupStateMachine, private val encryptionService: EncryptionService, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateMachine.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateMachine.kt index 4fbf3f3fd2..24a8be7f77 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateMachine.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateMachine.kt @@ -11,11 +11,12 @@ package io.element.android.features.securebackup.impl.setup import com.freeletics.flowredux.dsl.FlowReduxStateMachine +import dev.zacsweers.metro.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi -import javax.inject.Inject import com.freeletics.flowredux.dsl.State as MachineState -class SecureBackupSetupStateMachine @Inject constructor() : FlowReduxStateMachine( +@Inject +class SecureBackupSetupStateMachine : FlowReduxStateMachine( initialState = State.Initial ) { init { diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/tools/RecoveryKeyTools.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/tools/RecoveryKeyTools.kt index 7623324052..5805146c71 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/tools/RecoveryKeyTools.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/tools/RecoveryKeyTools.kt @@ -7,12 +7,13 @@ package io.element.android.features.securebackup.impl.tools -import javax.inject.Inject +import dev.zacsweers.metro.Inject private const val RECOVERY_KEY_LENGTH = 48 private const val BASE_58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" -class RecoveryKeyTools @Inject constructor() { +@Inject +class RecoveryKeyTools { fun isRecoveryKeyFormatValid(recoveryKey: String): Boolean { val recoveryKeyWithoutSpace = recoveryKey.replace("\\s+".toRegex(), "") return recoveryKeyWithoutSpace.length == RECOVERY_KEY_LENGTH && recoveryKeyWithoutSpace.all { BASE_58_ALPHABET.contains(it) } 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 3c6dd95b8e..487ec98bc5 100644 --- a/features/securebackup/impl/src/main/res/values-de/translations.xml +++ b/features/securebackup/impl/src/main/res/values-de/translations.xml @@ -2,17 +2,17 @@ "Backup deaktivieren" "Backup aktivieren" - "Speichern Sie Ihre verschlüsselte Identität und Ihre codierten Nachrichtenschlüssel auf dem Server. Auf diese Weise können Sie Ihren Nachrichtenverlauf auf allen neuen Geräten einsehen. %1$s." + "Speichere deine kryptographische Identität und die Nachrichtenschlüssel auf dem Server. Auf diese Weise kannst du deinen Nachrichtenverlauf auf neuen Geräten einsehen. %1$s." "Schlüsselspeicher" "Der Schlüsselspeicher muss aktiviert sein, um Datenwiederherstellung zu ermöglichen." "Schlüssel von diesem Gerät hochladen" "Schlüsselspeicherung zulassen" "Wiederherstellungsschlüssel ändern" - "Stellen Sie Ihre verschlüsselte Identität und Ihren Nachrichtenverlauf mit einem Wiederherstellungsschlüssel wieder her, falls Sie den Zugang zu allen Ihren Geräten verloren haben." + "Stelle deine kryptographische Identität und deinen Nachrichtenverlauf mit einem Wiederherstellungsschlüssel wieder her, falls du deine Geräte verloren hast." "Wiederherstellungsschlüssel eingeben" "Dein Schlüssel ist derzeit nicht synchronisiert." "Wiederherstellung einrichten" - "Erhalten Sie Zugriff auf Ihre verschlüsselten Nachrichten, wenn Sie all Ihre Geräte verlieren oder von %1$s überall abgemeldet sind." + "Erhalte Zugriff auf deine verschlüsselten Nachrichten, wenn du alle deine Geräte verloren hast oder überall von %1$s abgemeldet bist." "Öffne " "%1$s" @@ -20,14 +20,10 @@ "Desktop-Gerät" "Melde dich erneut bei deinem Konto an" - "Wenn Sie aufgefordert werden, Ihr Gerät zu verifizieren, wählen Sie \"%1$s\"" + "Bei der Aufforderung, dein Gerät zu verifizieren, wähle %1$s" "Alles zurücksetzen" "Folge den Anweisungen, um einen neuen Wiederherstellungsschlüssel zu erstellen" - - "Speichere deinen neuen " - "Wiederherstellungsschlüssel" - " in einem Passwortmanager oder einer verschlüsselten Notiz" - + "Verwahre deinen neuen Wiederherstellungsschlüssel in einem Passwortmanager oder einer verschlüsselten Datei" "Erstelle einen neuen " "Wiederherstellungsschlüssel" @@ -35,53 +31,49 @@ "Zurücksetzen fortsetzen" "Deine Kontodaten, Kontakte, Einstellungen und die Liste der Chats bleiben erhalten" - "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." - "Bestätigung unmöglich? Dann müssen Sie Ihre Identität zurücksetzen." + "Du verlierst alle bisherigen Nachrichten, wenn sie ausschließlich auf dem Server gespeichert sein sollten." + "Du musst alle deine bestehenden Geräte und Kontakte erneut verifizieren." + "Setze deine Identität nur dann zurück, wenn du keinen Zugriff mehr auf ein anderes angemeldetes Gerät hast und auch deinen Wiederherstellungsschlüssel verloren hast." + "Bestätigung unmöglich? Dann musst du deine Identität zurücksetzen." "Ausschalten" - "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." + "Du verlierst deine verschlüsselten Nachrichten, wenn du auf allen Geräten abgemeldet bist." + "Bist du sicher, dass du das Backup deaktivieren willst?" + "Das Löschen des Schlüsselspeichers entfernt deine kryptografische Identität und deine Nachrichtenschlüssel vom Server. Die folgenden Sicherheitsfunktionen werden deaktiviert:" + "Kein Nachrichtenverlauf für verschlüsselte Nachrichten auf neuen Geräten" + "Kein Zugriff auf verschlüsselten Nachrichten, wenn du überall von %1$s abgemeldet bist" + "Möchtest du die Speicherung der Schlüssel wirklich deaktivieren und entfernen?" + "Erhalte einen neuen Wiederherstellungsschlüssel wenn du deinen bisherigen verloren hast. Danach funktioniert dein alter Schlüssel nicht mehr." "Wiederherstellungsschlüssel erstellen" - "Geben Sie dies an niemanden weiter!" + "Teile das mit niemandem!" "Wiederherstellungsschlüssel geändert" "Wiederherstellungsschlüssel ändern?" - - "Neuen " - "Wiederherstellungsschlüssel" - " erstellen" - + "Neuen Wiederherstellungsschlüssel erstellen" "Sorge dafür, dass niemand diesen Bildschirm sehen kann!" - "Bitte versuchen Sie erneut, den Zugriff auf Ihren Schlüsselspeicher zu bestätigen." + "Bitte versuche erneut, den Zugriff auf deinen Schlüsselspeicher zu bestätigen." "Falscher Wiederherstellungsschlüssel" "Dies funktioniert auch mit einem Sicherheitsschlüssel oder Sicherheitsphrase." "Eingeben…" "Wiederherstellungschlüssel vergessen?" "Wiederherstellungsschlüssel bestätigt" - "Geben Sie Ihren Wiederherstellungsschlüssel ein" + "Gib deinen Wiederherstellungsschlüssel ein" "Wiederherstellungsschlüssel kopiert" "Generieren…" "Wiederherstellungsschlüssel speichern" - "Schreiben Sie Ihren Wiederherstellungsschlüssel in eine verschlüsselte Datei, oder in einem Passwort-Manager oder in einem Safe. " + "Bewahre den Wiederherstellungsschlüssel an einer sicheren Stelle auf, wie zum Beispiel in einem Passwort-Manager, in einer verschlüsselten Datei oder in einem Safe. " "Tippe, um den Wiederherstellungsschlüssel zu kopieren" "Speichere deinen Wiederherstellungsschlüssel" - "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." + "Nach diesem Schritt kannst du nicht mehr auf deinen neuen Wiederherstellungsschlüssel zugreifen." + "Hast du deinen Wiederherstellungsschlüssel gespeichert?" + "Dein Schlüsselspeicher wird durch einen Wiederherstellungsschlüssel geschützt. Wenn du nach der Einrichtung einen neuen Wiederherstellungsschlüssel benötigst, kannst du durch Auswahl von „Wiederherstellungsschlüssel ändern“ einen neuen erzeugen." "Wiederherstellungsschlüssel erstellen" - "Geben Sie dies an niemanden weiter!" + "Teile das mit niemandem!" "Einrichtung der Wiederherstellung erfolgreich" "Wiederherstellung einrichten" "Ja, zurücksetzen" "Das Zurücksetzen kann nicht rückgängig gemacht werden." - "Möchten Sie Ihre kryptografische Identität wirklich zurücksetzen?" + "Bist du sicher, dass du deine Identität zurücksetzen möchtest?" "Es ist ein unbekannter Fehler aufgetreten. Bitte überprüfe das Passwort deines Kontos und versuche es erneut." "Eingeben…" - "Bestätigen Sie, dass Sie Ihre Identität zurücksetzen möchten." + "Bestätige, dass du deine Identität zurücksetzen möchtest." "Gib dein Passwort ein, um fortzufahren" diff --git a/features/securebackup/impl/src/main/res/values-eo/translations.xml b/features/securebackup/impl/src/main/res/values-eo/translations.xml new file mode 100644 index 0000000000..4b8e1833a3 --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-eo/translations.xml @@ -0,0 +1,46 @@ + + + "Delete message backup" + "Store your account security and messages securely on the server. This will allow you to view your message history on any new devices. %1$s." + "Message backup" + "Turn on message backup to set it up." + "Upload messages from this device" + "Allow message backup" + "Change backup password" + "Restore your account security and message history with a backup password if you\'ve lost all your existing devices." + "Enter backup password" + "Your message backup is currently out of sync." + "Set up backup" + "When asked to confirm your device, select %1$s" + "Follow the instructions to create a new backup password" + "Save your new backup password in a password manager or encrypted note" + "You will need to confirm all your existing devices and verify contacts again" + "Only start fresh if you don\'t have access to another signed-in device and you\'ve lost your backup password." + "Can\'t confirm? You\'ll need to start fresh." + "Deleting message backup will remove your account security and messages from the server and turn off the following security features:" + "Are you sure you want to turn off message backup and delete it?" + "Get a new backup password if you\'ve lost your existing one. After changing your backup password, your old one will no longer work." + "Generate a new backup password" + "Backup password changed" + "Change backup password?" + "Create new backup password" + "Please try again to confirm access to your message backup." + "Incorrect backup password" + "You might have seen the terms \"recovery key\", \"security key\" or \"security phrase\" instead of \"backup password\". Don\'t worry, this is all the same." + "Lost your backup password?" + "Backup password confirmed" + "Enter your backup password" + "Copied backup password" + "Save backup password" + "Write down this backup password somewhere safe, like a password manager, encrypted note, or a physical safe." + "Tap to copy backup password" + "Save your backup password somewhere safe" + "You will not be able to access your new backup password after this step." + "Have you saved your backup password?" + "Your message backup is protected by a backup password. If you need a new backup password after setup, you can recreate it by selecting ‘Change backup password’." + "Generate your backup password" + "Backup setup successful" + "Set up backup" + "Are you sure you want to start fresh?" + "Confirm that you want to start fresh." + diff --git a/features/securebackup/impl/src/main/res/values-eu/translations.xml b/features/securebackup/impl/src/main/res/values-eu/translations.xml index 139127f8c7..86b1f31aca 100644 --- a/features/securebackup/impl/src/main/res/values-eu/translations.xml +++ b/features/securebackup/impl/src/main/res/values-eu/translations.xml @@ -17,6 +17,8 @@ "Berrezarri zure kontuaren enkriptazioa beste gailu bat erabiliz" "Jarraitu berrezarpenarekin" "Zure kontuaren xehetasunak, kontaktuak, hobespenak eta txat-zerrenda gordeko dira" + "Zerbitzarian soilik gordeta dagoen mezuen historia galduko duzu" + "Zure gailu eta kontaktu guztiak berriro egiaztatu beharko dituzu" "Ezin duzu baieztatu? Zure identitatea berrezarri beharko duzu." "Desaktibatu" "Enkriptatutako mezuak galduko dituzu gailu guztietan saioa amaitzen baduzu." diff --git a/features/securebackup/impl/src/main/res/values-fi/translations.xml b/features/securebackup/impl/src/main/res/values-fi/translations.xml index 7daf180c6b..4a0cc4e34d 100644 --- a/features/securebackup/impl/src/main/res/values-fi/translations.xml +++ b/features/securebackup/impl/src/main/res/values-fi/translations.xml @@ -37,7 +37,7 @@ "Luo uusi palautusavain" "Älä jaa tätä kenenkään kanssa!" "Palautusavain vaihdettu" - "Vaihda palautusavain?" + "Vaihdetaanko palautusavain?" "Luo uusi palautusavain" "Varmista, ettei kukaan näe tätä ruutua!" "Yritä uudelleen vahvistaaksesi pääsyn avainten säilytykseen." diff --git a/features/securebackup/impl/src/main/res/values-it/translations.xml b/features/securebackup/impl/src/main/res/values-it/translations.xml index e9b8849a92..fbe2c87931 100644 --- a/features/securebackup/impl/src/main/res/values-it/translations.xml +++ b/features/securebackup/impl/src/main/res/values-it/translations.xml @@ -12,13 +12,13 @@ "Inserisci la chiave di recupero" "L\'archiviazione delle chiavi non è sincronizzata." "Configura il recupero" - "Ottieni l\'accesso ai tuoi messaggi cifrati se perdi tutti i tuoi dispositivi o se sei disconnesso da %1$s ovunque." + "Ottieni l\'accesso ai tuoi messaggi criptati nel caso perdi tutti i dispositivi o vieni disconnesso da %1$s su tutti i dispositivi." "Apri %1$s in un dispositivo desktop" "Accedi nuovamente al tuo account" "Quando ti viene chiesto di verificare il tuo dispositivo, seleziona %1$s" "“Reimposta tutto”" "Segui le istruzioni per creare una nuova chiave di recupero" - "Salva la tua nuova chiave di recupero in un gestore di password o in una nota cifrata." + "Salva la tua nuova chiave di recupero in un gestore di password o in una nota criptata" "Reimposta la crittografia del tuo account utilizzando un altro dispositivo" "Continua il ripristino" "I dettagli del tuo account, i contatti, le preferenze e l\'elenco delle conversazioni verranno conservati" @@ -50,7 +50,7 @@ "Chiave di recupero copiata" "Generazione…" "Salva la chiave di recupero" - "Annota questa chiave di recupero in un posto sicuro, come un gestore di password, una nota cifrata o una cassaforte fisica." + "Annota questa chiave di recupero in un posto sicuro, come un gestore di password, una nota criptata o una cassaforte fisica." "Tocca per copiare la chiave di recupero" "Salva la tua chiave di recupero" "Dopo questo passaggio non potrai accedere alla nuova chiave di recupero." diff --git a/features/securebackup/impl/src/main/res/values-ko/translations.xml b/features/securebackup/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..804a907112 --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,70 @@ + + + "백업 비활성화" + "백업 활성화" + "암호화 신원 및 메시지 키를 서버에 안전하게 저장하세요. 이로써 새로운 기기에서 메시지 이력을 확인할 수 있습니다. %1$s." + "키 저장소" + "복구 설정을 하려면 키 저장을 켜야 합니다." + "이 장치에서 키 업로드" + "키 저장 허용" + "복구 키 변경" + "기존의 모든 기기를 분실한 경우, 복구 키를 사용하여 암호화 ID와 메시지 기록을 복구할 수 있습니다." + "복구 키를 입력하세요" + "현재 키 저장소가 동기화되지 않았습니다." + "복구 설정" + "모든 기기를 분실하거나 %1$s 에서 로그아웃된 경우에도 암호화된 메시지에 액세스할 수 있습니다." + "데스크톱 장치에서 %1$s 을 엽니다." + "계정에 다시 로그인하세요" + "장치를 확인하라는 메시지가 표시되면, %1$s 을 선택하세요" + "“모든 항목을 초기화합니다”" + "지침에 따라 새 복구 키를 만드세요." + "새 복구 키를 암호 관리자 또는 암호화된 메모에 저장하세요." + "다른 기기를 사용하여 계정의 암호화를 재설정하세요." + "계속 재설정" + "귀하의 계정 정보, 연락처, 기본 설정 및 채팅 목록은 보관됩니다" + "서버에만 저장된 모든 메시지 기록이 손실됩니다." + "기존 장치와 연락처를 모두 다시 확인해야 합니다." + "다른 로그인 기기에 액세스할 수 없고 복구 키를 분실한 경우에만 ID를 재설정하세요." + "확인할 수 없나요? 신원을 재설정해야 합니다." + "비활성화" + "모든 장치에서 로그아웃하면 암호화된 메시지가 삭제됩니다." + "정말로 백업을 비활성화하시겠어요?" + "키 저장소를 삭제하면 서버에서 암호화 신원 및 메시지 키가 삭제되며 다음과 같은 보안 기능이 비활성화됩니다:" + "새 장치에는 암호화된 메시지 기록이 남아 있지 않습니다." + "%1$s 에서 모든 세션이 종료되면 암호화된 메시지에 액세스할 수 없게 됩니다." + "정말로 키 저장소를 비활성화하고 삭제하시겠습니까?" + "기존 복구 키를 분실한 경우 새 복구 키를 받으세요. 복구 키를 변경하면 이전 키는 더 이상 사용할 수 없습니다." + "새로운 복구 키 생성" + "이 내용을 누구와도 공유하지 마십시오!" + "복구 키가 변경되었습니다." + "복구 키 변경하시겠습니까?" + "새 복구 키 만들기" + "아무도 이 화면을 볼 수 없도록 하세요!" + "키 저장소에 대한 액세스를 확인하시려면 다시 시도해 주세요." + "잘못된 복구 키" + "보안 키나 보안 문구를 가지고 있다면 이 방법도 작동합니다." + "입력…" + "복구 키를 분실하셨나요?" + "복구 키 확인됨" + "복구 키를 입력하세요" + "복사된 복구 키" + "생성 중…" + "복구 키 저장" + "이 복구 키를 암호 관리자, 암호화된 메모 또는 물리적 금고와 같은 안전한 곳에 기록해 두십시오." + "탭하여 복구 키 복사" + "복구 키를 안전한 곳에 보관하세요." + "이 단계를 완료하면 새 recovery key에 액세스할 수 없습니다." + "복구 키를 저장하셨습니까?" + "키 저장소는 복구 키로 보호됩니다. 설정 후 새로운 복구 키가 필요한 경우 \'복구 키 변경\'을 선택하여 재작성할 수 있습니다." + "복구 키 생성" + "이 내용을 누구와도 공유하지 마십시오!" + "복구 설정 성공" + "복구 설정" + "네, 지금 재설정하세요" + "이 과정은 되돌릴 수 없습니다." + "정말로 신원을 재설정하시겠습니까?" + "알 수 없는 오류가 발생했습니다. 계정 비밀번호가 올바른지 확인하고 다시 시도하십시오." + "입력…" + "신원 재설정을 확인하시겠습니까?" + "계정 비밀번호를 입력하여 진행하세요" + 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 524db97094..f613729791 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 @@ -1,49 +1,49 @@ - "Desativar o backup" + "Apagar o armazenamento de chaves" "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 dispositivos futuros. %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" + "Enviar chaves a partir deste dispositivo" + "Permitir 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" + "Digitar 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" + "Tenha acesso às suas mensagens criptografadas se você perder todos os seus dispositivos ou for desconectado do %1$s em todos os dispositivos." + "Abra o %1$s em um computador" + "Entre na sua conta novamente" + "Ao ser solicitado para verificar o 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" + "Continuar a redefinição" + "Os detalhes da sua conta, contatos, preferências e lista de conversas 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." + "Desativar" + "Você perderá suas mensagens criptografadas se for desconectado de todos os dispositivos." "Tem certeza de que deseja desativar o backup?" - "Desativar o backup removerá o backup da chave de criptografia atual e desativará outros recursos de segurança. Neste caso, você irá:" - "Não ter histórico de mensagens criptografadas em novos dispositivos" - "Perder o acesso às suas mensagens criptografadas se você estiver desconectado %1$s em todos os lugares" - "Tem certeza de que deseja desativar o backup?" + "Ao apagar o armazenamento de chaves, a sua identidade criptográfica e as chaves das mensagens serão apagadas do servidor e os seguintes recursos de segurança serão desativados:" + "Você não terá o histórico de mensagens criptografadas em dispositivos novos" + "Você perderá o acesso às suas mensagens criptografadas se for desconectado de %1$s em todos os dispositivos" + "Tem certeza de que deseja desativar o armazenamento de chaves e apagá-lo?" "Obtenha uma nova chave de recuperação caso tenha perdido a existente. Depois de alterar sua chave de recuperação, a antiga não funcionará mais." - "Gere uma nova chave de recuperação" + "Gerar uma nova chave de recuperação" "Não compartilhe isso com ninguém!" "Chave de recuperação alterada" "Alterar chave de recuperação?" - "Crie uma nova chave de recuperação" + "Criar 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…" + "Digite…" "Perdeu sua chave de recuperação?" "Chave de recuperação confirmada" "Digite sua chave de recuperação" @@ -52,10 +52,10 @@ "Salvar chave de recuperação" "Anote essa chave de recuperação em algum lugar seguro, como um gerenciador de senhas, uma nota criptografada ou um cofre físico." "Toque para copiar a chave de recuperação" - "Salve sua chave de recuperação" + "Guarde sua chave de recuperação em um lugar seguro" "Você não poderá acessar sua nova chave de recuperação após essa etapa." "Você salvou sua chave de recuperação?" - "Seu backup das conversas é protegido por uma chave de recuperação. Se precisar de uma nova chave de recuperação após a configuração, você pode recriá-la selecionando “Alterar chave de recuperação”." + "Seu armazenamento de chaves é protegido por uma chave de recuperação. Se precisar de uma nova chave de recuperação após a configuração, você pode recriá-la selecionando “Alterar chave de recuperação”." "Gere sua chave de recuperação" "Não compartilhe isso com ninguém!" "Configuração de recuperação bem-sucedida" @@ -64,7 +64,7 @@ "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…" + "Digite…" "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-ro/translations.xml b/features/securebackup/impl/src/main/res/values-ro/translations.xml index 08d46373bb..2eda1a576b 100644 --- a/features/securebackup/impl/src/main/res/values-ro/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ro/translations.xml @@ -2,12 +2,13 @@ "Dezactivați backupul" "Activați backupul" - "Stocați identitatea criptografică și cheile de mesaje în siguranță pe server. Acest lucru vă va permite să vizualizați istoricul mesajelor pe orice dispozitiv nou. %1$s." + "Stocați identitatea criptografică și cheile de mesaje în siguranță pe server. Acest lucru vă va permite să vizualizați mesajele anterioare pe orice dispozitiv nou. %1$s." "Backup" + "Stocarea cheilor trebuie activată pentru a configura recuperarea." "Încărcați cheile de pe acest dispozitiv" "Permiteți stocarea cheilor" "Schimbați cheia de recuperare" - "Recuperați-vă identitatea criptografică și istoricul mesajelor cu o cheie de recuperare dacă ați pierdut toate dispozitivele existente." + "Recuperați-vă identitatea criptografică și mesajele anterioare cu o cheie de recuperare dacă ați pierdut toate dispozitivele existente." "Introduceți cheia de recuperare" "Backup-ul pentru chat nu este sincronizat în prezent." "Configurați recuperarea" @@ -21,7 +22,7 @@ "Resetați criptarea contului dumneavoastră folosind un alt dispozitiv" "Continuați resetarea" "Detaliile contului, contactele, preferințele și lista de chat vor fi păstrate" - "Veți pierde mesajele istorice care au fost stocate doar pe server" + "Veți pierde mesajele anterioare care au fost stocate doar pe server" "Va trebui să verificați din nou toate dispozitivele și contactele existente" "Resetați-vă identitatea numai dacă nu aveți acces la un alt dispozitiv conectat și ați pierdut cheia de recuperare." "Nu puteți confirma? Va trebui să vă resetați identitatea." @@ -29,7 +30,7 @@ "Veți pierde mesajele criptate dacă sunteți deconectat de pe toate dispozitivele." "Sunteți sigur că doriți să dezactivați backup-ul?" "Dezactivarea backup-ului va șterge backup-ul curent și va dezactiva alte măsuri de securitate. În acest caz, veți:" - "Nu veți avea istoricul mesajelor criptate pe dispozitive noi" + "Nu veți avea mesajele anterioare criptate pe dispozitive noi" "Veți pierde accesul la mesajele criptate dacă sunteți deconectat de pe %1$s peste tot" "Sunteți sigur că doriți să dezactivați backup-ul?" "Obțineți o nouă cheie de recuperare dacă ați pierdut-o pe cea existentă. După schimbarea cheii de recuperare, cea veche nu va mai funcționa." @@ -45,6 +46,7 @@ "Introduceți…" "Ați pierdut cheia de recuperare?" "Cheia de recuperare confirmată" + "Introduceți cheia de recuperare" "Cheia de recuperare copiată" "Se generează…" "Salvați cheia de recuperare" diff --git a/features/securebackup/impl/src/main/res/values-uz/translations.xml b/features/securebackup/impl/src/main/res/values-uz/translations.xml index 9f2cb7f163..88dff88802 100644 --- a/features/securebackup/impl/src/main/res/values-uz/translations.xml +++ b/features/securebackup/impl/src/main/res/values-uz/translations.xml @@ -3,11 +3,29 @@ "Zaxiralashni o\'chirib qo\'ying" "Zaxiralashni yoqing" "Kryptografik shaxsiyatingizni va xabar kalitlaringizni serverda xavfsiz saqlang. Bu sizga har qanday yangi qurilmalarda xabar tarixingizni ko\'rish imkonini beradi. %1$s." - "Zaxira" + "Kalitlar ombori" + "Tiklashni sozlash uchun kalitlar xotirasini yoqish kerak." + "Bu qurilmadan kalitlarni yuklash" + "Kalit saqlashga ruxsat berish" "Qayta tiklash kalitini o\'zgartiring" - "Sizning chat zaxirangiz hozirda sinxronlashtirilmagan." + "Agar barcha mavjud qurilmalaringizni yoʻqotgan boʻlsangiz, tiklash kaliti yordamida kriptografik shaxsingizni va xabarlar tarixingizni qayta tiklang." + "Tiklash kalitini kiriting" + "Kalit xotirasi hozirda sinxronlanmagan." "Qayta tiklashni sozlang" "Agar barcha qurilmalaringizni yo‘qotib qo‘ysangiz yoki tizimdan chiqqan bo‘lsangiz, shifrlangan xabarlaringizga ruxsat oling%1$s hamma joyda." + "%1$s ni kompyuterda oching" + "Hisobingizga qaytadan kiring" + "Qurilmangizni tasdiqlash soʻralganda, %1$s ni tanlang" + "ʻʻHammasini asliga qaytarishʼʼ" + "Yangi tiklash kalitini yaratish uchun koʻrsatmalarga amal qiling" + "Yangi tiklash kalitingizni parol menejeriga yoki shifrlangan yozuvga saqlab qoʻying" + "Hisobingiz shifrini boshqa qurilma orqali asliga qaytaring" + "Qayta tiklashda davom eting" + "Hisob maʼlumotlaringiz, kontaktlaringiz, sozlamalaringiz va suhbatlar roʻyxatingiz saqlanib qoladi" + "Faqat serverda saqlangan har qanday xabarlar tarixi oʻchib ketadi" + "Barcha mavjud qurilma va kontaktlarni qayta tasdiqlashingiz kerak boʻladi" + "Agar boshqa hisobga kirilgan qurilmaga kira olmasangiz va tiklash kaliti yo‘qolgan bo‘lsa, shaxsingizni tiklang." + "Tasdiqlanmadimi? Shaxsingizni tiklashingiz kerak." "O\'chirish" "Agar barcha qurilmalardan chiqqan boʻlsangiz, shifrlangan xabarlaringizni yoʻqotasiz." "Haqiqatan ham zaxiralashni o‘chirib qo‘ymoqchimisiz?" @@ -20,10 +38,15 @@ "Buni hech kimga ulashmang!" "Qayta tiklash kaliti oʻzgartirildi" "Qayta tiklash kaliti almashtirilsinmi?" + "Yangi tiklash kalitini yaratish" "Hech kim bu ekranni kora olmasligiga ishonch hosil qiling!" + "Kalit xotirasiga kirishni tasdiqlash uchun qayta urinib koʻring." + "Notoʻgʻri tiklash kaliti" "Agar sizda xavfsizlik kaliti yoki xavfsizlik iborasi bolsa, bu ham ishlaydi." "Kirish…" + "Tiklanish kalitingizni yoʻqotdingizmi?" "Qayta tiklash kaliti tasdiqlandi" + "Qayta tiklash kalitingizni kiriting" "Qayta tiklash kaliti nusxalandi" "Yaratilmoqda…" "Qayta tiklash kalitini saqlang" @@ -37,5 +60,11 @@ "Buni hech kimga ulashmang!" "Qayta tiklash muvaffaqiyatli sozlandi" "Qayta tiklashni sozlang" + "Ha, hozir asliga qaytarish" + "Bu jarayonni ortga qaytarib boʻlmaydi." + "Haqiqatan ham shaxsingizni qayta tiklamoqchimisiz?" + "Noma’lum xato yuz berdi. Iltimos, hisobingiz parolining to‘g‘riligini tekshiring va qaytadan urinib ko‘ring." "Kirish…" + "Shaxsingizni tiklashni tasdiqlang." + "Davom etish uchun hisobingiz parolini kiriting" diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPointTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPointTest.kt new file mode 100644 index 0000000000..7741b5141a --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPointTest.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.securebackup.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.securebackup.api.SecureBackupEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultSecureBackupEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultSecureBackupEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + SecureBackupFlowNode( + buildContext = buildContext, + plugins = plugins, + ) + } + val callback = object : SecureBackupEntryPoint.Callback { + override fun onDone() = lambdaError() + } + val params = SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.ResetIdentity) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .callback(callback) + .build() + assertThat(result).isInstanceOf(SecureBackupFlowNode::class.java) + assertThat(result.plugins).contains(params) + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/share/impl/build.gradle.kts b/features/share/impl/build.gradle.kts index c059a24982..4c62a06352 100644 --- a/features/share/impl/build.gradle.kts +++ b/features/share/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -22,7 +23,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.appconfig) @@ -41,16 +42,8 @@ dependencies { api(libs.statemachine) api(projects.features.share.api) - 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) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.preferences.test) - testImplementation(projects.tests.testutils) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/DefaultShareEntryPoint.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/DefaultShareEntryPoint.kt index 3ec2f42d80..fe65c60b73 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/DefaultShareEntryPoint.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/DefaultShareEntryPoint.kt @@ -10,14 +10,15 @@ package io.element.android.features.share.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 dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.share.api.ShareEntryPoint import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultShareEntryPoint @Inject constructor() : ShareEntryPoint { +@Inject +class DefaultShareEntryPoint : ShareEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): ShareEntryPoint.NodeBuilder { val plugins = ArrayList() 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 b2b1dd36e6..b3db111820 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 @@ -15,7 +15,9 @@ import android.content.pm.ResolveInfo import android.net.Uri import android.os.Build import androidx.core.content.IntentCompat -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.compat.queryIntentActivitiesCompat import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAny @@ -25,10 +27,8 @@ import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeFile import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeText import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.ApplicationContext import timber.log.Timber -import javax.inject.Inject interface ShareIntentHandler { data class UriToShare( @@ -49,7 +49,8 @@ interface ShareIntentHandler { } @ContributesBinding(AppScope::class) -class DefaultShareIntentHandler @Inject constructor( +@Inject +class DefaultShareIntentHandler( @ApplicationContext private val context: Context, ) : ShareIntentHandler { override suspend fun handleIncomingShareIntent( diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt index 6d04db6e43..e268419920 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt @@ -18,9 +18,9 @@ import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.core.plugin.Plugin -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.share.api.ShareEntryPoint import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs @@ -31,7 +31,8 @@ import io.element.android.libraries.roomselect.api.RoomSelectMode import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -class ShareNode @AssistedInject constructor( +@AssistedInject +class ShareNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: SharePresenter.Factory, 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 b66765f472..d3222edf5e 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 @@ -11,9 +11,9 @@ import android.content.Intent import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState @@ -31,7 +31,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlin.coroutines.cancellation.CancellationException -class SharePresenter @AssistedInject constructor( +@AssistedInject +class SharePresenter( @Assisted private val intent: Intent, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, @@ -42,7 +43,7 @@ class SharePresenter @AssistedInject constructor( private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider, ) : Presenter { @AssistedFactory - interface Factory { + fun interface Factory { fun create(intent: Intent): SharePresenter } diff --git a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/DefaultShareEntryPointTest.kt b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/DefaultShareEntryPointTest.kt new file mode 100644 index 0000000000..66ee853d26 --- /dev/null +++ b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/DefaultShareEntryPointTest.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.share.impl + +import android.content.Intent +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.share.api.ShareEntryPoint +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class DefaultShareEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() = runTest { + val entryPoint = DefaultShareEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + ShareNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { createSharePresenter() }, + roomSelectEntryPoint = object : RoomSelectEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomSelectEntryPoint.NodeBuilder { + lambdaError() + } + }, + ) + } + val callback = object : ShareEntryPoint.Callback { + override fun onDone(roomIds: List) = lambdaError() + } + val params = ShareEntryPoint.Params( + intent = Intent(), + ) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .callback(callback) + .build() + assertThat(result).isInstanceOf(ShareNode::class.java) + assertThat(result.plugins).contains(ShareNode.Inputs(params.intent)) + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt index 7ddb7d0ae4..b1b39b2e80 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 @@ -158,23 +158,23 @@ class SharePresenterTest { sendFileResult.assertions().isCalledOnce() } } - - private fun TestScope.createSharePresenter( - intent: Intent = Intent(), - shareIntentHandler: ShareIntentHandler = FakeShareIntentHandler(), - matrixClient: MatrixClient = FakeMatrixClient(), - mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(), - activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(), - mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), - ): SharePresenter { - return SharePresenter( - intent = intent, - sessionCoroutineScope = this, - shareIntentHandler = shareIntentHandler, - matrixClient = matrixClient, - mediaPreProcessor = mediaPreProcessor, - activeRoomsHolder = activeRoomsHolder, - mediaOptimizationConfigProvider = mediaOptimizationConfigProvider, - ) - } +} + +internal fun TestScope.createSharePresenter( + intent: Intent = Intent(), + shareIntentHandler: ShareIntentHandler = FakeShareIntentHandler(), + matrixClient: MatrixClient = FakeMatrixClient(), + mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(), + activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(), + mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), +): SharePresenter { + return SharePresenter( + intent = intent, + sessionCoroutineScope = this, + shareIntentHandler = shareIntentHandler, + matrixClient = matrixClient, + mediaPreProcessor = mediaPreProcessor, + activeRoomsHolder = activeRoomsHolder, + mediaOptimizationConfigProvider = mediaOptimizationConfigProvider, + ) } diff --git a/features/signedout/impl/build.gradle.kts b/features/signedout/impl/build.gradle.kts index 7e89186d27..f314cb8283 100644 --- a/features/signedout/impl/build.gradle.kts +++ b/features/signedout/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -16,7 +17,7 @@ android { namespace = "io.element.android.features.signedout.impl" } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.signedout.api) @@ -27,13 +28,7 @@ dependencies { 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) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.sessionStorage.implMemory) testImplementation(projects.libraries.sessionStorage.test) - testImplementation(projects.tests.testutils) } diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/DefaultSignedOutEntryPoint.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/DefaultSignedOutEntryPoint.kt index b239d1bb8a..91def6db8d 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/DefaultSignedOutEntryPoint.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/DefaultSignedOutEntryPoint.kt @@ -10,14 +10,15 @@ package io.element.android.features.signedout.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.signedout.api.SignedOutEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultSignedOutEntryPoint @Inject constructor() : SignedOutEntryPoint { +@Inject +class DefaultSignedOutEntryPoint : SignedOutEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): SignedOutEntryPoint.NodeBuilder { val plugins = ArrayList() diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutNode.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutNode.kt index 2e081b2b89..1bacc78932 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutNode.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutNode.kt @@ -12,16 +12,17 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.SessionId @ContributesNode(AppScope::class) -class SignedOutNode @AssistedInject constructor( +@AssistedInject +class SignedOutNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: SignedOutPresenter.Factory, @@ -31,7 +32,7 @@ class SignedOutNode @AssistedInject constructor( ) : NodeInputs private val inputs: Inputs = inputs() - private val presenter = presenterFactory.create(inputs.sessionId.value) + private val presenter = presenterFactory.create(inputs.sessionId) @Composable override fun View(modifier: Modifier) { 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 773954c3e0..7c9d1e6d83 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 @@ -9,43 +9,43 @@ package io.element.android.features.signedout.impl 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 androidx.compose.runtime.rememberCoroutineScope -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.sessionstorage.api.SessionStore +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -class SignedOutPresenter @AssistedInject constructor( - // Cannot inject SessionId - @Assisted private val sessionId: String, +@AssistedInject +class SignedOutPresenter( + @Assisted private val sessionId: SessionId, private val sessionStore: SessionStore, private val buildMeta: BuildMeta, ) : Presenter { @AssistedFactory - interface Factory { - fun create(sessionId: String): SignedOutPresenter + fun interface Factory { + fun create(sessionId: SessionId): SignedOutPresenter } @Composable override fun present(): SignedOutState { - val sessions by remember { - sessionStore.sessionsFlow() - }.collectAsState(initial = emptyList()) val signedOutSession by remember { - derivedStateOf { sessions.firstOrNull { it.userId == sessionId } } - } + sessionStore.sessionsFlow().map { sessions -> + sessions.firstOrNull { it.userId == sessionId.value } + } + }.collectAsState(initial = null) val coroutineScope = rememberCoroutineScope() fun handleEvents(event: SignedOutEvents) { when (event) { SignedOutEvents.SignInAgain -> coroutineScope.launch { - sessionStore.removeSession(sessionId) + sessionStore.removeSession(sessionId.value) } } } diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt index 55e29c9e26..a7b95a8537 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt @@ -43,5 +43,9 @@ private fun aSessionData( passphrase = null, sessionPath = "/a/path/to/a/session", cachePath = "/a/path/to/a/cache", + position = 0, + lastUsageIndex = 0, + userDisplayName = null, + userAvatarUrl = null, ) } 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 4b631a7020..d73c34d4f9 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 @@ - "Sie haben Ihr Passwort in einer anderen Sitzung geändert" - "Sie haben diese Sitzung aus einer anderen Sitzung gelöscht" + "Du hast dein Passwort in einer anderen Sitzung geändert" + "Du hast diese Sitzung aus einer anderen Sitzung gelöscht" "Der Administrator deines Servers hat deinen Zugang ungültig gemacht" - "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" + "Möglicherweise wurdest du aus einem der folgenden Gründe abgemeldet. Bitte melde dich erneut an, um %s weiter zu nutzen." + "Du bist abgemeldet" diff --git a/features/signedout/impl/src/main/res/values-ko/translations.xml b/features/signedout/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..bb46d3a4aa --- /dev/null +++ b/features/signedout/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,8 @@ + + + "다른 세션에서 비밀번호를 변경하셨습니다." + "다른 세션에서 세션을 삭제했습니다." + "귀하의 서버 관리자가 귀하의 액세스를 무효화했습니다." + "아래 나열된 이유 중 하나로 인해 로그아웃되었을 수 있습니다. 계속 사용하려면 다시 로그인하세요 %s ." + "로그아웃 되었습니다" + diff --git a/features/signedout/impl/src/main/res/values-pt-rBR/translations.xml b/features/signedout/impl/src/main/res/values-pt-rBR/translations.xml index 3c31806492..7c920f172e 100644 --- a/features/signedout/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/signedout/impl/src/main/res/values-pt-rBR/translations.xml @@ -1,8 +1,8 @@ "Você alterou sua senha em outra sessão" - "Você excluiu essa sessão através de outra sessão" + "Você apagou essa sessão através de outra sessão" "O administrador do seu servidor invalidou seu acesso" - "Você pode ter sido desconectado por um dos motivos listados abaixo. Faça login novamente para continuar usando %s." + "Você pode ter sido desconectado por um dos motivos listados abaixo. Entre novamente para continuar usando o %s." "Você está desconectado" diff --git a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/DefaultSignedOutEntryPointTest.kt b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/DefaultSignedOutEntryPointTest.kt new file mode 100644 index 0000000000..6366ba5ea1 --- /dev/null +++ b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/DefaultSignedOutEntryPointTest.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.signedout.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.features.signedout.api.SignedOutEntryPoint +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultSignedOutEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultSignedOutEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + SignedOutNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { sessionId -> + assertThat(sessionId).isEqualTo(A_SESSION_ID) + createSignedOutPresenter() + } + ) + } + val params = SignedOutEntryPoint.Params(A_SESSION_ID) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .build() + assertThat(result).isInstanceOf(SignedOutNode::class.java) + assertThat(result.plugins).contains(SignedOutNode.Inputs(params.sessionId)) + } +} diff --git a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt index 9cfa4eb667..e53c0af112 100644 --- a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt +++ b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt @@ -12,10 +12,11 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.test.AN_APPLICATION_NAME import io.element.android.libraries.matrix.test.A_SESSION_ID 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.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest @@ -26,21 +27,19 @@ class SignedOutPresenterTest { @get:Rule val warmUpRule = WarmUpRule() - private val appName = "AppName" - @Test fun `present - initial state`() = runTest { val aSessionData = aSessionData() - val sessionStore = InMemorySessionStore().apply { - storeData(aSessionData) - } + val sessionStore = InMemorySessionStore( + initialList = listOf(aSessionData) + ) val presenter = createSignedOutPresenter(sessionStore = sessionStore) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { skipItems(1) val initialState = awaitItem() - assertThat(initialState.appName).isEqualTo(appName) + assertThat(initialState.appName).isEqualTo(AN_APPLICATION_NAME) assertThat(initialState.signedOutSession).isEqualTo(aSessionData) } } @@ -48,9 +47,9 @@ class SignedOutPresenterTest { @Test fun `present - sign in again`() = runTest { val aSessionData = aSessionData() - val sessionStore = InMemorySessionStore().apply { - storeData(aSessionData) - } + val sessionStore = InMemorySessionStore( + initialList = listOf(aSessionData) + ) val presenter = createSignedOutPresenter(sessionStore = sessionStore) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -64,15 +63,15 @@ class SignedOutPresenterTest { assertThat(sessionStore.getAllSessions()).isEmpty() } } - - private fun createSignedOutPresenter( - sessionId: SessionId = A_SESSION_ID, - sessionStore: SessionStore = InMemorySessionStore(), - ): SignedOutPresenter { - return SignedOutPresenter( - sessionId = sessionId.value, - sessionStore = sessionStore, - buildMeta = aBuildMeta(applicationName = appName), - ) - } +} + +internal fun createSignedOutPresenter( + sessionId: SessionId = A_SESSION_ID, + sessionStore: SessionStore = InMemorySessionStore(), +): SignedOutPresenter { + return SignedOutPresenter( + sessionId = sessionId, + sessionStore = sessionStore, + buildMeta = aBuildMeta(applicationName = AN_APPLICATION_NAME), + ) } diff --git a/libraries/session-storage/impl-memory/build.gradle.kts b/features/space/api/build.gradle.kts similarity index 52% rename from libraries/session-storage/impl-memory/build.gradle.kts rename to features/space/api/build.gradle.kts index 6401a0587f..dd19efefec 100644 --- a/libraries/session-storage/impl-memory/build.gradle.kts +++ b/features/space/api/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2023, 2024 New Vector Ltd. + * Copyright 2025 New Vector Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial * Please see LICENSE files in the repository root for full details. @@ -9,10 +9,10 @@ plugins { } android { - namespace = "io.element.android.libraries.sessionstorage.impl.memory" + namespace = "io.element.android.features.space.api" } dependencies { - implementation(projects.libraries.sessionStorage.api) - implementation(libs.coroutines.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) } diff --git a/features/space/api/src/main/kotlin/io/element/android/features/space/api/SpaceEntryPoint.kt b/features/space/api/src/main/kotlin/io/element/android/features/space/api/SpaceEntryPoint.kt new file mode 100644 index 0000000000..bdea93f3ef --- /dev/null +++ b/features/space/api/src/main/kotlin/io/element/android/features/space/api/SpaceEntryPoint.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.features.space.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.core.RoomId + +interface SpaceEntryPoint : FeatureEntryPoint { + fun nodeBuilder( + parentNode: Node, + buildContext: BuildContext, + ): NodeBuilder + + interface NodeBuilder { + fun inputs(inputs: Inputs): NodeBuilder + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + data class Inputs( + val roomId: RoomId + ) : NodeInputs + + interface Callback : Plugin { + fun onOpenRoom(roomId: RoomId, viaParameters: List) + } +} diff --git a/features/space/impl/build.gradle.kts b/features/space/impl/build.gradle.kts new file mode 100644 index 0000000000..b6afbdcb05 --- /dev/null +++ b/features/space/impl/build.gradle.kts @@ -0,0 +1,49 @@ +import extension.setupDependencyInjection +import extension.testCommonDependencies + +/* + * Copyright 2025 New 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.space.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +setupDependencyInjection() + +dependencies { + 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) + implementation(projects.libraries.androidutils) + implementation(projects.libraries.deeplink.api) + implementation(projects.services.analytics.api) + implementation(libs.coil.compose) + implementation(projects.libraries.featureflag.api) + implementation(projects.features.invite.api) + implementation(projects.libraries.previewutils) + api(projects.features.space.api) + + testCommonDependencies(libs, true) + testImplementation(projects.services.analytics.test) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.featureflag.test) + testImplementation(projects.features.invite.test) +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPoint.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPoint.kt new file mode 100644 index 0000000000..8591978417 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPoint.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.features.space.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.features.space.api.SpaceEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.SessionScope + +@ContributesBinding(SessionScope::class) +@Inject +class DefaultSpaceEntryPoint : SpaceEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): SpaceEntryPoint.NodeBuilder { + val plugins = mutableSetOf() + return object : SpaceEntryPoint.NodeBuilder { + override fun inputs(inputs: SpaceEntryPoint.Inputs): SpaceEntryPoint.NodeBuilder { + plugins.add(inputs) + return this + } + + override fun callback(callback: SpaceEntryPoint.Callback): SpaceEntryPoint.NodeBuilder { + plugins.add(callback) + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins = plugins.toList()) + } + } + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt new file mode 100644 index 0000000000..fffdeb6246 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2025 New 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.space.impl + +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +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.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.space.api.SpaceEntryPoint +import io.element.android.features.space.impl.di.SpaceFlowGraph +import io.element.android.features.space.impl.leave.LeaveSpaceNode +import io.element.android.features.space.impl.root.SpaceNode +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.inputs +import io.element.android.libraries.di.DependencyInjectionGraphOwner +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 kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +@AssistedInject +class SpaceFlowNode( + @Assisted val buildContext: BuildContext, + @Assisted plugins: List, + matrixClient: MatrixClient, + graphFactory: SpaceFlowGraph.Factory, +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins, +), DependencyInjectionGraphOwner { + private val inputs: SpaceEntryPoint.Inputs = inputs() + private val callback = plugins.filterIsInstance().single() + private val spaceRoomList = matrixClient.spaceService.spaceRoomList(inputs.roomId) + override val graph = graphFactory.create(spaceRoomList) + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Root : NavTarget + + @Parcelize + data object Leave : NavTarget + } + + override fun onBuilt() { + super.onBuilt() + lifecycle.subscribe( + onDestroy = { + spaceRoomList.destroy() + } + ) + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Leave -> { + createNode(buildContext, listOf(inputs)) + } + NavTarget.Root -> { + val callback = object : SpaceNode.Callback { + override fun onOpenRoom(roomId: RoomId, viaParameters: List) { + callback.onOpenRoom(roomId, viaParameters) + } + + override fun onLeaveSpace() { + backstack.push(NavTarget.Leave) + } + } + createNode(buildContext, listOf(inputs, callback)) + } + } + } + + @Composable + override fun View(modifier: Modifier) = BackstackView() +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/di/SpaceFlowGraph.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/di/SpaceFlowGraph.kt new file mode 100644 index 0000000000..b1dac522b4 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/di/SpaceFlowGraph.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.features.space.impl.di + +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.GraphExtension +import dev.zacsweers.metro.Provides +import io.element.android.libraries.architecture.NodeFactoriesBindings +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList + +@GraphExtension(SpaceFlowScope::class) +interface SpaceFlowGraph : NodeFactoriesBindings { + @ContributesTo(SessionScope::class) + @GraphExtension.Factory + interface Factory { + fun create(@Provides spaceRoomList: SpaceRoomList): SpaceFlowGraph + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/di/SpaceFlowScope.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/di/SpaceFlowScope.kt new file mode 100644 index 0000000000..77fb07f871 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/di/SpaceFlowScope.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.space.impl.di + +abstract class SpaceFlowScope private constructor() diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceEvents.kt new file mode 100644 index 0000000000..3c963a0bf5 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceEvents.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.space.impl.leave + +import io.element.android.libraries.matrix.api.core.RoomId + +sealed interface LeaveSpaceEvents { + data object SelectAllRooms : LeaveSpaceEvents + data object DeselectAllRooms : LeaveSpaceEvents + data class ToggleRoomSelection(val roomId: RoomId) : LeaveSpaceEvents + data object LeaveSpace : LeaveSpaceEvents + data object CloseError : LeaveSpaceEvents +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt new file mode 100644 index 0000000000..df313481a1 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.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.features.space.impl.leave + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.space.impl.di.SpaceFlowScope + +@ContributesNode(SpaceFlowScope::class) +@AssistedInject +class LeaveSpaceNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: LeaveSpacePresenter, +) : Node(buildContext, plugins = plugins) { + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + LeaveSpaceView( + state = state, + onCancel = ::navigateUp, + modifier = modifier + ) + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt new file mode 100644 index 0000000000..7af18c1b6d --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.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.space.impl.leave + +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.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import dev.zacsweers.metro.Inject +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentSet +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlin.jvm.optionals.getOrNull + +@Inject +class LeaveSpacePresenter( + private val spaceRoomList: SpaceRoomList, +) : Presenter { + @Composable + override fun present(): LeaveSpaceState { + val coroutineScope = rememberCoroutineScope() + val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState() + val leaveSpaceAction = remember { + mutableStateOf>(AsyncAction.Uninitialized) + } + val selectedRoomIds = remember { + mutableStateOf>(persistentSetOf()) + } + val joinedSpaceRooms by produceState(emptyList()) { + // TODO Get the joined room from the SDK, should also have the isLastAdmin boolean + val rooms = emptyList() + // By default select all rooms + selectedRoomIds.value = rooms.map { it.roomId }.toPersistentSet() + value = rooms + } + val selectableSpaceRooms by produceState>>( + initialValue = AsyncData.Uninitialized, + key1 = joinedSpaceRooms, + key2 = selectedRoomIds.value, + ) { + value = AsyncData.Success( + joinedSpaceRooms.map { + SelectableSpaceRoom( + spaceRoom = it, + // TODO Get this value from the SDK + isLastAdmin = false, + isSelected = selectedRoomIds.value.contains(it.roomId), + ) + }.toPersistentList() + ) + } + + fun handleEvents(event: LeaveSpaceEvents) { + when (event) { + LeaveSpaceEvents.DeselectAllRooms -> { + selectedRoomIds.value = persistentSetOf() + } + LeaveSpaceEvents.SelectAllRooms -> { + selectedRoomIds.value = selectableSpaceRooms.dataOrNull() + .orEmpty() + .filter { it.isLastAdmin.not() } + .map { it.spaceRoom.roomId } + .toPersistentSet() + } + is LeaveSpaceEvents.ToggleRoomSelection -> { + val currentSet = selectedRoomIds.value + selectedRoomIds.value = if (currentSet.contains(event.roomId)) { + currentSet - event.roomId + } else { + currentSet + event.roomId + } + .toPersistentSet() + } + LeaveSpaceEvents.LeaveSpace -> coroutineScope.leaveSpace( + leaveSpaceAction = leaveSpaceAction, + selectedRoomIds = selectedRoomIds.value, + ) + LeaveSpaceEvents.CloseError -> { + leaveSpaceAction.value = AsyncAction.Uninitialized + } + } + } + + return LeaveSpaceState( + spaceName = currentSpace.getOrNull()?.name, + selectableSpaceRooms = selectableSpaceRooms, + leaveSpaceAction = leaveSpaceAction.value, + eventSink = ::handleEvents, + ) + } + + private fun CoroutineScope.leaveSpace( + leaveSpaceAction: MutableState>, + @Suppress("unused") selectedRoomIds: Set, + ) = launch { + runUpdatingState(leaveSpaceAction) { + // TODO SDK API call to leave all the rooms and space + Result.failure(Exception("Not implemented")) + } + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt new file mode 100644 index 0000000000..f63eef2333 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.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.space.impl.leave + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import kotlinx.collections.immutable.ImmutableList + +data class LeaveSpaceState( + val spaceName: String?, + val selectableSpaceRooms: AsyncData>, + val leaveSpaceAction: AsyncAction, + val eventSink: (LeaveSpaceEvents) -> Unit, +) { + private val rooms = selectableSpaceRooms.dataOrNull().orEmpty() + private val partition = rooms.partition { it.isLastAdmin } + private val lastAdminRooms = partition.first + private val selectableRooms = partition.second + + /** + * True if we should show the quick action to select/deselect all rooms. + */ + val showQuickAction = selectableRooms.isNotEmpty() + + /** + * True if there all the selectable rooms are selected. + */ + val areAllSelected = selectableRooms.all { it.isSelected } + + /** + * True if there are rooms but the user is the last admin in all of them. + */ + val hasOnlyLastAdminRoom = lastAdminRooms.isNotEmpty() && selectableRooms.isEmpty() + + /** + * Number of selected rooms. + */ + val selectedRoomsCount = selectableRooms.count { it.isSelected } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt new file mode 100644 index 0000000000..6795cba3a7 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.leave + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.room.join.JoinRule +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.previewutils.room.aSpaceRoom +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +class LeaveSpaceStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aLeaveSpaceState(), + aLeaveSpaceState( + spaceName = null, + selectableSpaceRooms = AsyncData.Success(persistentListOf()), + ), + aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + persistentListOf( + aSelectableSpaceRoom( + spaceRoom = aSpaceRoom( + name = "A long space name that should be truncated", + worldReadable = true, + ), + isLastAdmin = true, + ), + aSelectableSpaceRoom( + spaceRoom = aSpaceRoom( + joinRule = JoinRule.Private, + ), + isSelected = false, + ), + ) + ) + ), + aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + persistentListOf( + aSelectableSpaceRoom( + spaceRoom = aSpaceRoom( + worldReadable = true, + ), + isLastAdmin = true, + ), + aSelectableSpaceRoom( + spaceRoom = aSpaceRoom( + joinRule = JoinRule.Private, + ), + isSelected = true, + ), + ) + ) + ), + aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + persistentListOf( + aSelectableSpaceRoom( + spaceRoom = aSpaceRoom( + worldReadable = true, + ), + isLastAdmin = true, + ), + ) + ), + ), + aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + persistentListOf( + aSelectableSpaceRoom( + spaceRoom = aSpaceRoom( + worldReadable = true, + ), + isLastAdmin = true, + ), + aSelectableSpaceRoom( + spaceRoom = aSpaceRoom(), + isLastAdmin = true, + ), + ) + ), + ), + aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + List(10) { aSelectableSpaceRoom() }.toPersistentList() + ), + leaveSpaceAction = AsyncAction.Loading, + ), + aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + List(10) { aSelectableSpaceRoom() }.toPersistentList() + ), + leaveSpaceAction = AsyncAction.Failure(Exception("An error")), + ), + aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Failure(Exception("An error")), + ), + ) +} + +fun aLeaveSpaceState( + spaceName: String? = "Space name", + selectableSpaceRooms: AsyncData> = AsyncData.Uninitialized, + leaveSpaceAction: AsyncAction = AsyncAction.Uninitialized, +) = LeaveSpaceState( + spaceName = spaceName, + selectableSpaceRooms = selectableSpaceRooms, + leaveSpaceAction = leaveSpaceAction, + eventSink = { } +) + +fun aSelectableSpaceRoom( + spaceRoom: SpaceRoom = aSpaceRoom(), + isLastAdmin: Boolean = false, + isSelected: Boolean = false, +) = SelectableSpaceRoom( + spaceRoom = spaceRoom, + isLastAdmin = isLastAdmin, + isSelected = isSelected, +) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt new file mode 100644 index 0000000000..2a3cd73ea9 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt @@ -0,0 +1,345 @@ +/* + * Copyright 2025 New 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.space.impl.leave + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.selection.toggleable +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.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +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.space.impl.R +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.components.BigIcon +import io.element.android.libraries.designsystem.components.async.AsyncActionView +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.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.components.Button +import io.element.android.libraries.designsystem.theme.components.Checkbox +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconSource +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.api.room.join.JoinRule +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.ui.strings.CommonPlurals +import io.element.android.libraries.ui.strings.CommonStrings + +/** + * https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=3947-68767&t=GTf1cLkAf6UCQDan-0 + */ +@Composable +fun LeaveSpaceView( + state: LeaveSpaceState, + onCancel: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + containerColor = ElementTheme.colors.bgCanvasDefault, + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .imePadding() + .consumeWindowInsets(padding) + .fillMaxSize() + .padding(16.dp) + ) { + LeaveSpaceHeader( + state = state, + onBackClick = onCancel, + ) + LazyColumn( + modifier = Modifier + .weight(1f), + ) { + when (state.selectableSpaceRooms) { + is AsyncData.Success -> { + // List rooms where the user is the only admin + state.selectableSpaceRooms.data.forEach { selectableSpaceRoom -> + item { + SpaceItem( + selectableSpaceRoom = selectableSpaceRoom, + showCheckBox = state.hasOnlyLastAdminRoom.not(), + onClick = { + state.eventSink(LeaveSpaceEvents.ToggleRoomSelection(selectableSpaceRoom.spaceRoom.roomId)) + } + ) + } + } + } + is AsyncData.Failure -> item { + AsyncFailure( + throwable = state.selectableSpaceRooms.error, + onRetry = null, + ) + } + is AsyncData.Loading, + AsyncData.Uninitialized -> item { + AsyncLoading() + } + } + } + LeaveSpaceButtons( + showLeaveButton = state.selectableSpaceRooms is AsyncData.Success, + selectedRoomsCount = state.selectedRoomsCount, + onLeaveSpace = { + state.eventSink(LeaveSpaceEvents.LeaveSpace) + }, + onCancel = onCancel, + ) + } + } + + AsyncActionView( + async = state.leaveSpaceAction, + onSuccess = { /* Nothing to do, the screen will be dismissed automatically */ }, + onErrorDismiss = { state.eventSink(LeaveSpaceEvents.CloseError) }, + ) +} + +@Composable +private fun LeaveSpaceHeader( + state: LeaveSpaceState, + onBackClick: () -> Unit, +) { + Column { + TopAppBar( + navigationIcon = { + BackButton(onClick = onBackClick) + }, + title = {}, + ) + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 0.dp, bottom = 8.dp, start = 24.dp, end = 24.dp), + iconStyle = BigIcon.Style.AlertSolid, + title = stringResource( + R.string.screen_leave_space_title, + state.spaceName ?: stringResource(CommonStrings.common_space) + ), + subTitle = + if (state.selectableSpaceRooms is AsyncData.Success && state.selectableSpaceRooms.data.isNotEmpty()) { + if (state.hasOnlyLastAdminRoom) { + stringResource(R.string.screen_leave_space_subtitle_only_last_admin) + } else { + stringResource(R.string.screen_leave_space_subtitle) + } + } else { + null + }, + ) + if (state.showQuickAction) { + if (state.areAllSelected) { + Text( + modifier = Modifier + .align(Alignment.End) + .clickable { + state.eventSink(LeaveSpaceEvents.DeselectAllRooms) + } + .padding(vertical = 8.dp, horizontal = 8.dp), + text = stringResource(CommonStrings.common_deselect_all), + color = ElementTheme.colors.textActionPrimary, + style = ElementTheme.typography.fontBodyMdMedium, + ) + } else { + Text( + modifier = Modifier + .align(Alignment.End) + .clickable { + state.eventSink(LeaveSpaceEvents.SelectAllRooms) + } + .padding(vertical = 8.dp, horizontal = 8.dp), + text = stringResource(CommonStrings.common_select_all), + color = ElementTheme.colors.textActionPrimary, + style = ElementTheme.typography.fontBodyMdMedium, + ) + } + } + } +} + +@Composable +private fun LeaveSpaceButtons( + showLeaveButton: Boolean, + selectedRoomsCount: Int, + onLeaveSpace: () -> Unit, + onCancel: () -> Unit, +) { + ButtonColumnMolecule( + modifier = Modifier.padding(top = 16.dp) + ) { + if (showLeaveButton) { + val text = if (selectedRoomsCount > 0) { + pluralStringResource(R.plurals.screen_leave_space_submit, selectedRoomsCount, selectedRoomsCount) + } else { + stringResource(CommonStrings.action_leave_space) + } + Button( + modifier = Modifier.fillMaxWidth(), + text = text, + leadingIcon = IconSource.Vector(CompoundIcons.Leave()), + onClick = onLeaveSpace, + destructive = true, + ) + } + TextButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_cancel), + onClick = onCancel, + ) + } +} + +@Composable +private fun SpaceItem( + selectableSpaceRoom: SelectableSpaceRoom, + showCheckBox: Boolean, + onClick: () -> Unit, +) { + val room = selectableSpaceRoom.spaceRoom + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 66.dp) + .toggleable( + value = selectableSpaceRoom.isSelected, + role = Role.Checkbox, + enabled = selectableSpaceRoom.isLastAdmin.not(), + onValueChange = { onClick() } + ) + .clickable( + enabled = selectableSpaceRoom.isLastAdmin.not(), + // TODO + onClickLabel = null, + role = Role.Checkbox, + onClick = onClick, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Avatar( + modifier = Modifier.padding(horizontal = 16.dp), + avatarData = room.getAvatarData(AvatarSize.LeaveSpaceRoom), + avatarType = if (room.isSpace) AvatarType.Space() else AvatarType.Room(), + ) + Column( + modifier = Modifier.weight(1f), + ) { + Text( + modifier = Modifier + .padding(end = 16.dp), + text = room.name ?: stringResource( + if (room.isSpace) { + CommonStrings.common_no_space_name + } else { + CommonStrings.common_no_room_name + }, + ), + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontBodyLgMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + if (room.joinRule == JoinRule.Private) { + // Picto for private + Icon( + modifier = Modifier + .size(16.dp) + .padding(end = 4.dp), + imageVector = CompoundIcons.LockSolid(), + contentDescription = null, + tint = ElementTheme.colors.iconTertiary, + ) + } else if (room.worldReadable) { + // Picto for world readable + Icon( + modifier = Modifier + .size(16.dp) + .padding(end = 4.dp), + imageVector = CompoundIcons.Public(), + contentDescription = null, + tint = ElementTheme.colors.iconTertiary, + ) + } + // Number of members + val subTitle = buildString { + append( + pluralStringResource( + CommonPlurals.common_member_count, + room.numJoinedMembers, + room.numJoinedMembers + ) + ) + if (selectableSpaceRoom.isLastAdmin) { + append(" ") + append(stringResource(R.string.screen_leave_space_last_admin_info)) + } + } + Text( + modifier = Modifier.padding(end = 16.dp), + text = subTitle, + color = ElementTheme.colors.textSecondary, + style = ElementTheme.typography.fontBodyMdRegular, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + if (showCheckBox) { + Checkbox( + checked = selectableSpaceRoom.isSelected, + onCheckedChange = null, + enabled = selectableSpaceRoom.isLastAdmin.not(), + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun LeaveSpaceViewPreview( + @PreviewParameter(LeaveSpaceStateProvider::class) state: LeaveSpaceState, +) = ElementPreview { + LeaveSpaceView( + state = state, + onCancel = {}, + ) +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/SelectableSpaceRoom.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/SelectableSpaceRoom.kt new file mode 100644 index 0000000000..6247a9e48f --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/SelectableSpaceRoom.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.space.impl.leave + +import io.element.android.libraries.matrix.api.spaces.SpaceRoom + +data class SelectableSpaceRoom( + val spaceRoom: SpaceRoom, + val isLastAdmin: Boolean, + val isSelected: Boolean, +) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt new file mode 100644 index 0000000000..ab94ef719b --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.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.space.impl.root + +import io.element.android.libraries.matrix.api.spaces.SpaceRoom + +sealed interface SpaceEvents { + data object LoadMore : SpaceEvents + data class Join(val spaceRoom: SpaceRoom) : SpaceEvents + data object ClearFailures : SpaceEvents + data class AcceptInvite(val spaceRoom: SpaceRoom) : SpaceEvents + data class DeclineInvite(val spaceRoom: SpaceRoom) : SpaceEvents +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt new file mode 100644 index 0000000000..52c3472182 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.root + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.lifecycleScope +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView +import io.element.android.features.space.impl.di.SpaceFlowScope +import io.element.android.libraries.androidutils.R +import io.element.android.libraries.androidutils.system.startSharePlainTextIntent +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.coroutines.launch +import timber.log.Timber + +@ContributesNode(SpaceFlowScope::class) +@AssistedInject +class SpaceNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: SpacePresenter, + private val matrixClient: MatrixClient, + private val spaceRoomList: SpaceRoomList, + private val acceptDeclineInviteView: AcceptDeclineInviteView, +) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onOpenRoom(roomId: RoomId, viaParameters: List) + fun onLeaveSpace() + } + + private val callback = plugins.filterIsInstance().single() + + private fun onShareRoom(context: Context) = lifecycleScope.launch { + matrixClient.getRoom(spaceRoomList.roomId)?.use { room -> + room.getPermalink() + .onSuccess { permalink -> + context.startSharePlainTextIntent( + activityResultLauncher = null, + chooserTitle = context.getString(CommonStrings.common_share_space), + text = permalink, + noActivityFoundMessage = context.getString(R.string.error_no_compatible_app_found) + ) + } + .onFailure { + Timber.e(it) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + val context = LocalContext.current + SpaceView( + state = state, + onBackClick = ::navigateUp, + onLeaveSpaceClick = { + callback.onLeaveSpace() + }, + onRoomClick = { spaceRoom -> + callback.onOpenRoom(spaceRoom.roomId, spaceRoom.via) + }, + onShareSpace = { + onShareRoom(context) + }, + acceptDeclineInviteView = { + acceptDeclineInviteView.Render( + state = state.acceptDeclineInviteState, + onAcceptInviteSuccess = { roomId -> + callback.onOpenRoom(roomId, emptyList()) + }, + onDeclineInviteSuccess = { roomId -> + // No action needed + }, + modifier = Modifier + ) + }, + modifier = modifier + ) + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt new file mode 100644 index 0000000000..7a3481bb73 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import dev.zacsweers.metro.Inject +import im.vector.app.features.analytics.plan.JoinedRoom +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.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.mapState +import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.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.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.join.JoinRoom +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList +import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentMap +import kotlinx.collections.immutable.toPersistentSet +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlin.jvm.optionals.getOrNull + +@Inject +class SpacePresenter( + private val spaceRoomList: SpaceRoomList, + private val client: MatrixClient, + private val seenInvitesStore: SeenInvitesStore, + private val joinRoom: JoinRoom, + private val acceptDeclineInvitePresenter: Presenter, + @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, +) : Presenter { + @Composable + override fun present(): SpaceState { + LaunchedEffect(Unit) { + paginate() + } + val hideInvitesAvatar by client.rememberHideInvitesAvatar() + val seenSpaceInvites by remember { + seenInvitesStore.seenRoomIds().map { it.toPersistentSet() } + }.collectAsState(persistentSetOf()) + + val localCoroutineScope = rememberCoroutineScope() + val children by spaceRoomList.spaceRoomsFlow.collectAsState(emptyList()) + val hasMoreToLoad by remember { + spaceRoomList.paginationStatusFlow.mapState { status -> + when (status) { + is SpaceRoomList.PaginationStatus.Idle -> status.hasMoreToLoad + SpaceRoomList.PaginationStatus.Loading -> true + } + } + }.collectAsState() + + val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState() + val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap>()) } + + LaunchedEffect(children) { + // Remove joined children from the join actions + val joinedChildren = children + .filter { it.state == CurrentUserMembership.JOINED } + .map { it.roomId } + setJoinActions(joinActions - joinedChildren) + } + + val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() + + fun handleEvents(event: SpaceEvents) { + when (event) { + SpaceEvents.LoadMore -> localCoroutineScope.paginate() + is SpaceEvents.Join -> { + sessionCoroutineScope.joinRoom(event.spaceRoom, joinActions, setJoinActions) + } + SpaceEvents.ClearFailures -> { + val failedActions = joinActions + .filterValues { it is AsyncAction.Failure } + .mapValues { AsyncAction.Uninitialized } + setJoinActions(joinActions + failedActions) + } + is SpaceEvents.AcceptInvite -> { + acceptDeclineInviteState.eventSink( + AcceptDeclineInviteEvents.AcceptInvite(event.spaceRoom.toInviteData()) + ) + } + is SpaceEvents.DeclineInvite -> { + acceptDeclineInviteState.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(invite = event.spaceRoom.toInviteData(), shouldConfirm = true, blockUser = false) + ) + } + } + } + return SpaceState( + currentSpace = currentSpace.getOrNull(), + children = children.toPersistentList(), + seenSpaceInvites = seenSpaceInvites, + hideInvitesAvatar = hideInvitesAvatar, + hasMoreToLoad = hasMoreToLoad, + joinActions = joinActions.toPersistentMap(), + acceptDeclineInviteState = acceptDeclineInviteState, + eventSink = ::handleEvents, + ) + } + + private fun CoroutineScope.joinRoom( + spaceRoom: SpaceRoom, + joinActions: Map>, + setJoinActions: (Map>) -> Unit + ) = launch { + setJoinActions(joinActions + mapOf(spaceRoom.roomId to AsyncAction.Loading)) + joinRoom.invoke( + roomIdOrAlias = spaceRoom.roomId.toRoomIdOrAlias(), + serverNames = spaceRoom.via, + trigger = JoinedRoom.Trigger.SpaceHierarchy, + ).onFailure { + setJoinActions(joinActions + mapOf(spaceRoom.roomId to AsyncAction.Failure(it))) + } + } + + private fun CoroutineScope.paginate() = launch { + spaceRoomList.paginate() + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt new file mode 100644 index 0000000000..ed6bc3dcf7 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.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.features.space.impl.root + +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.ImmutableSet + +data class SpaceState( + val currentSpace: SpaceRoom?, + val children: ImmutableList, + val seenSpaceInvites: ImmutableSet, + val hideInvitesAvatar: Boolean, + val hasMoreToLoad: Boolean, + val joinActions: ImmutableMap>, + val acceptDeclineInviteState: AcceptDeclineInviteState, + val eventSink: (SpaceEvents) -> Unit +) { + fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading + val hasAnyFailure: Boolean = joinActions.values.any { + it is AsyncAction.Failure + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt new file mode 100644 index 0000000000..c0a88c38f5 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.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.space.impl.root + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState +import io.element.android.libraries.architecture.AsyncAction +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.spaces.SpaceRoom +import io.element.android.libraries.previewutils.room.aSpaceRoom +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap +import kotlinx.collections.immutable.toImmutableSet + +open class SpaceStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSpaceState(), + aSpaceState( + parentSpace = aSpaceRoom( + name = null, + numJoinedMembers = 5, + childrenCount = 10, + worldReadable = true, + ), + hasMoreToLoad = true, + ), + aSpaceState( + hasMoreToLoad = true, + children = aListOfSpaceRooms(), + ), + aSpaceState( + hasMoreToLoad = false, + children = aListOfSpaceRooms(), + joiningRooms = setOf(RoomId("!spaceId0:example.com")), + ) + // Add other states here + ) +} + +fun aSpaceState( + parentSpace: SpaceRoom? = aSpaceRoom( + numJoinedMembers = 5, + childrenCount = 10, + worldReadable = true, + roomId = RoomId("!spaceId0:example.com"), + ), + children: List = emptyList(), + seenSpaceInvites: Set = emptySet(), + joiningRooms: Set = emptySet(), + joinActions: Map> = joiningRooms.associateWith { AsyncAction.Loading }, + hideInvitesAvatar: Boolean = false, + hasMoreToLoad: Boolean = false, + acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), + eventSink: (SpaceEvents) -> Unit = { }, +) = SpaceState( + currentSpace = parentSpace, + children = children.toImmutableList(), + seenSpaceInvites = seenSpaceInvites.toImmutableSet(), + hideInvitesAvatar = hideInvitesAvatar, + hasMoreToLoad = hasMoreToLoad, + joinActions = joinActions.toImmutableMap(), + acceptDeclineInviteState = acceptDeclineInviteState, + eventSink = eventSink, +) + +private fun aListOfSpaceRooms(): List { + return listOf( + aSpaceRoom( + roomId = RoomId("!spaceId0:example.com"), + state = null, + ), + aSpaceRoom( + roomId = RoomId("!spaceId1:example.com"), + state = CurrentUserMembership.JOINED, + ), + aSpaceRoom( + roomId = RoomId("!spaceId2:example.com"), + state = CurrentUserMembership.INVITED, + ), + ) +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt new file mode 100644 index 0000000000..870e294ba5 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -0,0 +1,348 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.root + +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.foundation.lazy.LazyColumn +import androidx.compose.material3.ExperimentalMaterial3Api +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.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.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.atomic.molecules.InviteButtonsRowMolecule +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.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +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.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.CurrentUserMembership +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.ui.components.JoinButton +import io.element.android.libraries.matrix.ui.components.SpaceHeaderView +import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay + +@Composable +fun SpaceView( + state: SpaceState, + onBackClick: () -> Unit, + onRoomClick: (spaceRoom: SpaceRoom) -> Unit, + onShareSpace: () -> Unit, + onLeaveSpaceClick: () -> Unit, + modifier: Modifier = Modifier, + acceptDeclineInviteView: @Composable () -> Unit, +) { + Scaffold( + modifier = modifier, + topBar = { + SpaceViewTopBar( + currentSpace = state.currentSpace, + onBackClick = onBackClick, + onLeaveSpaceClick = onLeaveSpaceClick, + onShareSpace = onShareSpace, + ) + }, + content = { padding -> + Box( + modifier = Modifier.padding(padding) + ) { + SpaceViewContent( + state = state, + onRoomClick = onRoomClick + ) + JoinRoomFailureEffect( + hasAnyFailure = state.hasAnyFailure, + eventSink = state.eventSink + ) + acceptDeclineInviteView() + } + }, + ) +} + +@Composable +private fun JoinRoomFailureEffect( + hasAnyFailure: Boolean, + eventSink: (SpaceEvents) -> Unit, +) { + val asyncIndicatorState = rememberAsyncIndicatorState() + val updatedEventSink by rememberUpdatedState(eventSink) + AsyncIndicatorHost(modifier = Modifier, asyncIndicatorState) + LaunchedEffect(hasAnyFailure) { + if (hasAnyFailure) { + asyncIndicatorState.enqueue { + AsyncIndicator.Failure(text = stringResource(CommonStrings.common_something_went_wrong)) + } + delay(AsyncIndicator.DURATION_SHORT) + updatedEventSink(SpaceEvents.ClearFailures) + } else { + asyncIndicatorState.clear() + } + } +} + +@Composable +private fun SpaceViewContent( + state: SpaceState, + onRoomClick: (spaceRoom: SpaceRoom) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn(modifier.fillMaxSize()) { + val currentSpace = state.currentSpace + if (currentSpace != null) { + item { + SpaceHeaderView( + avatarData = currentSpace.getAvatarData(AvatarSize.SpaceHeader), + name = currentSpace.name, + topic = currentSpace.topic, + joinRule = currentSpace.joinRule, + heroes = currentSpace.heroes.toImmutableList(), + numberOfMembers = currentSpace.numJoinedMembers, + numberOfRooms = currentSpace.childrenCount, + ) + } + } + state.children.forEach { spaceRoom -> + item { + val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED + val isCurrentlyJoining = state.isJoining(spaceRoom.roomId) + SpaceRoomItemView( + spaceRoom = spaceRoom, + showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites, + hideAvatars = isInvitation && state.hideInvitesAvatar, + onClick = { + onRoomClick(spaceRoom) + }, + onLongClick = { + // TODO + }, + trailingAction = spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) { + state.eventSink(SpaceEvents.Join(spaceRoom)) + }, + bottomAction = spaceRoom.inviteButtons( + onAcceptClick = { + state.eventSink(SpaceEvents.AcceptInvite(spaceRoom)) + }, + onDeclineClick = { + state.eventSink(SpaceEvents.DeclineInvite(spaceRoom)) + } + ) + ) + } + } + if (state.hasMoreToLoad) { + item { + LoadingMoreIndicator(eventSink = state.eventSink) + } + } + } +} + +@Composable +private fun LoadingMoreIndicator( + eventSink: (SpaceEvents) -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + strokeWidth = 2.dp, + modifier = Modifier.padding(vertical = 8.dp) + ) + val latestEventSink by rememberUpdatedState(eventSink) + LaunchedEffect(Unit) { + latestEventSink(SpaceEvents.LoadMore) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SpaceViewTopBar( + currentSpace: SpaceRoom?, + onBackClick: () -> Unit, + @Suppress("unused") onLeaveSpaceClick: () -> Unit, + onShareSpace: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + navigationIcon = { + BackButton(onClick = onBackClick) + }, + title = { + if (currentSpace != null) { + SpaceAvatarAndNameRow( + name = currentSpace.name, + avatarData = currentSpace.getAvatarData(AvatarSize.TimelineRoom), + ) + } + }, + actions = { + var showMenu by remember { mutableStateOf(false) } + IconButton( + onClick = { showMenu = !showMenu } + ) { + Icon( + imageVector = CompoundIcons.OverflowVertical(), + contentDescription = null, + ) + } + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + DropdownMenuItem( + onClick = { + showMenu = false + onShareSpace() + }, + text = { Text(stringResource(id = CommonStrings.action_share)) }, + leadingIcon = { + Icon( + imageVector = CompoundIcons.ShareAndroid(), + tint = ElementTheme.colors.iconSecondary, + contentDescription = null, + ) + } + ) + /* + // TODO re-enable when we have SDK APIs to leave a space + DropdownMenuItem( + onClick = { + showMenu = false + onLeaveSpaceClick() + }, + text = { Text(stringResource(id = CommonStrings.action_leave)) }, + leadingIcon = { + Icon( + imageVector = CompoundIcons.Leave(), + tint = ElementTheme.colors.iconSecondary, + contentDescription = null, + ) + } + ) + */ + } + }, + ) +} + +@Composable +private fun SpaceAvatarAndNameRow( + name: String?, + avatarData: AvatarData, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Avatar( + avatarData = avatarData, + avatarType = AvatarType.Space(), + ) + Text( + modifier = Modifier + .padding(horizontal = 8.dp) + .semantics { + heading() + }, + text = name ?: stringResource(CommonStrings.common_no_space_name), + style = ElementTheme.typography.fontBodyLgMedium, + fontStyle = FontStyle.Italic.takeIf { name == null }, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +private fun SpaceRoom.trailingAction( + isCurrentlyJoining: Boolean, + onClick: () -> Unit +): @Composable (() -> Unit)? { + return when (state) { + null, CurrentUserMembership.LEFT -> { + { + JoinButton( + showProgress = isCurrentlyJoining, + onClick = onClick, + ) + } + } + else -> null + } +} + +private fun SpaceRoom.inviteButtons( + onAcceptClick: () -> Unit, + onDeclineClick: () -> Unit, +): @Composable (() -> Unit)? { + return when (state) { + CurrentUserMembership.INVITED -> { + @Composable { + InviteButtonsRowMolecule( + onAcceptClick = onAcceptClick, + onDeclineClick = onDeclineClick, + ) + } + } + else -> null + } +} + +@PreviewsDayNight +@Composable +internal fun SpaceViewPreview( + @PreviewParameter(SpaceStateProvider::class) state: SpaceState +) = ElementPreview { + SpaceView( + state = state, + onRoomClick = {}, + onShareSpace = {}, + onLeaveSpaceClick = {}, + acceptDeclineInviteView = {}, + onBackClick = {}, + ) +} diff --git a/features/space/impl/src/main/res/values-cs/translations.xml b/features/space/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..8a0886e786 --- /dev/null +++ b/features/space/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,11 @@ + + + "(Správce)" + + "Opustit %1$d místnost a prostor" + "Opustit %1$d místnosti a prostor" + "Opustit %1$d místností a prostor" + + "Tím budete také odstraněni ze všech místností v tomto prostoru." + "Opustit %1$s?" + diff --git a/features/space/impl/src/main/res/values-cy/translations.xml b/features/space/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 0000000000..b9e5823f97 --- /dev/null +++ b/features/space/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,5 @@ + + + "Bydd hyn hefyd yn eich tynnu o bob ystafell yn y gofod hwn." + "Gadael %1$s ?" + diff --git a/features/space/impl/src/main/res/values-da/translations.xml b/features/space/impl/src/main/res/values-da/translations.xml new file mode 100644 index 0000000000..ee6c2fcfb9 --- /dev/null +++ b/features/space/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,10 @@ + + + "Administrator" + + "Forlad %1$d rum og klynge" + "Forlad %1$d rum og klynger" + + "Vælg de rum, du vil forlade, som du ikke er den eneste administrator for:" + "Forlad %1$s?" + diff --git a/features/space/impl/src/main/res/values-de/translations.xml b/features/space/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..faab578205 --- /dev/null +++ b/features/space/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,10 @@ + + + "(Admin)" + + "%1$d Chat und Space verlassen" + "%1$d Chats und Space verlassen" + + "Dadurch wirst du auch aus allen Chats in diesem Space entfernt." + "%1$s verlassen?" + diff --git a/features/space/impl/src/main/res/values-et/translations.xml b/features/space/impl/src/main/res/values-et/translations.xml new file mode 100644 index 0000000000..d60171dc89 --- /dev/null +++ b/features/space/impl/src/main/res/values-et/translations.xml @@ -0,0 +1,9 @@ + + + + "Lahku %1$d-st jututoast ja kogukonnast" + "Lahku %1$d-st jututoast ja kogukonnast" + + "Sellega eemaldad end ka kõikidest antud kogukonna jututubadest." + "Kas lahkud %1$s kogukonnast?" + diff --git a/features/space/impl/src/main/res/values-fi/translations.xml b/features/space/impl/src/main/res/values-fi/translations.xml new file mode 100644 index 0000000000..45d41d7011 --- /dev/null +++ b/features/space/impl/src/main/res/values-fi/translations.xml @@ -0,0 +1,5 @@ + + + "Tämä poistaa sinut myös kaikista tämän tilan huoneista." + "Haluatko poistua tilasta %1$s?" + diff --git a/features/space/impl/src/main/res/values-fr/translations.xml b/features/space/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..5ff48f6c39 --- /dev/null +++ b/features/space/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,10 @@ + + + "(Admin)" + + "Quitter %1$d salon et l’espace" + "Quitter %1$d salons et l’espace" + + "Sélectionnez les salons que vous souhaitez quitter et dont vous n’êtes pas le seul administrateur:" + "Quitter %1$s?" + diff --git a/features/space/impl/src/main/res/values-hu/translations.xml b/features/space/impl/src/main/res/values-hu/translations.xml new file mode 100644 index 0000000000..8196780cf9 --- /dev/null +++ b/features/space/impl/src/main/res/values-hu/translations.xml @@ -0,0 +1,10 @@ + + + "(Adminisztrátor)" + + "%1$d szoba és tér elhagyása" + "%1$d szoba és tér elhagyása" + + "Ez a tér összes szobájából is eltávolítja." + "Kilép innen: %1$s?" + diff --git a/features/space/impl/src/main/res/values-pt/translations.xml b/features/space/impl/src/main/res/values-pt/translations.xml new file mode 100644 index 0000000000..75e1f5431e --- /dev/null +++ b/features/space/impl/src/main/res/values-pt/translations.xml @@ -0,0 +1,9 @@ + + + + "Sair do espaço e de %1$d sala" + "Sair do espaço e de %1$d salas" + + "Também irás sair de todas as salas deste espaço." + "Sair de %1$s?" + diff --git a/features/space/impl/src/main/res/values-zh-rTW/translations.xml b/features/space/impl/src/main/res/values-zh-rTW/translations.xml new file mode 100644 index 0000000000..419ea40233 --- /dev/null +++ b/features/space/impl/src/main/res/values-zh-rTW/translations.xml @@ -0,0 +1,5 @@ + + + "這也會將您從此空間中的所有聊天室移除。" + "離開 %1$s?" + diff --git a/features/space/impl/src/main/res/values/localazy.xml b/features/space/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..07c5468ce6 --- /dev/null +++ b/features/space/impl/src/main/res/values/localazy.xml @@ -0,0 +1,11 @@ + + + "(Admin)" + + "Leave %1$d room and space" + "Leave %1$d rooms and space" + + "Select the rooms you’d like to leave which you\'re not the only administrator for:" + "You will not be removed from the following room(s) because you\'re the only administrator:" + "Leave %1$s?" + diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt new file mode 100644 index 0000000000..007c28e3a6 --- /dev/null +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.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.space.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.space.api.SpaceEntryPoint +import io.element.android.features.space.impl.di.FakeSpaceFlowGraph +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.FakeMatrixClient +import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList +import io.element.android.libraries.matrix.test.spaces.FakeSpaceService +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultSpaceEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultSpaceEntryPoint() + val nodeInputs = SpaceEntryPoint.Inputs(A_ROOM_ID) + val parentNode = TestParentNode.create { buildContext, plugins -> + SpaceFlowNode( + buildContext = buildContext, + plugins = plugins, + matrixClient = FakeMatrixClient( + spaceService = FakeSpaceService( + spaceRoomListResult = { _: RoomId -> FakeSpaceRoomList(A_ROOM_ID) } + ) + ), + graphFactory = FakeSpaceFlowGraph.Factory + ) + } + val callback = object : SpaceEntryPoint.Callback { + override fun onOpenRoom(roomId: RoomId, viaParameters: List) = lambdaError() + } + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .inputs(nodeInputs) + .callback(callback) + .build() + assertThat(result).isInstanceOf(SpaceFlowNode::class.java) + assertThat(result.plugins).contains(nodeInputs) + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/di/FakeSpaceFlowGraph.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/di/FakeSpaceFlowGraph.kt new file mode 100644 index 0000000000..09263ff52d --- /dev/null +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/di/FakeSpaceFlowGraph.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.space.impl.di + +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.AssistedNodeFactory +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList +import kotlin.reflect.KClass + +class FakeSpaceFlowGraph : SpaceFlowGraph { + object Factory : SpaceFlowGraph.Factory { + override fun create(spaceRoomList: SpaceRoomList): SpaceFlowGraph { + return FakeSpaceFlowGraph() + } + } + + override fun nodeFactories(): Map, AssistedNodeFactory<*>> { + return emptyMap() + } +} diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt new file mode 100644 index 0000000000..ee8962a345 --- /dev/null +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.space.impl.leave + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList +import io.element.android.libraries.matrix.test.A_SPACE_NAME +import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList +import io.element.android.libraries.previewutils.room.aSpaceRoom +import io.element.android.tests.testutils.test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LeaveSpacePresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createLeaveSpacePresenter() + presenter.test { + val state = awaitItem() + assertThat(state.spaceName).isNull() + assertThat(state.selectableSpaceRooms).isEqualTo(AsyncData.Uninitialized) + assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) + skipItems(1) + } + } + + @Test + fun `present - current space name`() = runTest { + val fakeSpaceRoomList = FakeSpaceRoomList() + val presenter = createLeaveSpacePresenter( + spaceRoomList = fakeSpaceRoomList, + ) + presenter.test { + val state = awaitItem() + advanceUntilIdle() + assertThat(state.spaceName).isNull() + val aSpace = aSpaceRoom( + name = A_SPACE_NAME + ) + fakeSpaceRoomList.emitCurrentSpace(aSpace) + skipItems(1) + assertThat(awaitItem().spaceName).isEqualTo(A_SPACE_NAME) + } + } + + private fun createLeaveSpacePresenter( + spaceRoomList: SpaceRoomList = FakeSpaceRoomList(), + ): LeaveSpacePresenter { + return LeaveSpacePresenter( + spaceRoomList = spaceRoomList, + ) + } +} diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateTest.kt new file mode 100644 index 0000000000..eaf3f1a783 --- /dev/null +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateTest.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.features.space.impl.leave + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncData +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import org.junit.Test + +class LeaveSpaceStateTest { + @Test + fun `test loading`() { + val sut = aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Loading() + ) + assertThat(sut.showQuickAction).isFalse() + assertThat(sut.areAllSelected).isTrue() + assertThat(sut.hasOnlyLastAdminRoom).isFalse() + assertThat(sut.selectedRoomsCount).isEqualTo(0) + } + + @Test + fun `test no rooms`() { + val sut = aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + persistentListOf() + ) + ) + assertThat(sut.showQuickAction).isFalse() + assertThat(sut.areAllSelected).isTrue() + assertThat(sut.hasOnlyLastAdminRoom).isFalse() + assertThat(sut.selectedRoomsCount).isEqualTo(0) + } + + @Test + fun `test no last admin, 1 selected, 1 not selected`() { + val sut = aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + listOf( + aSelectableSpaceRoom(isLastAdmin = false, isSelected = true), + aSelectableSpaceRoom(isLastAdmin = false, isSelected = false), + ).toPersistentList() + ) + ) + assertThat(sut.showQuickAction).isTrue() + assertThat(sut.areAllSelected).isFalse() + assertThat(sut.hasOnlyLastAdminRoom).isFalse() + assertThat(sut.selectedRoomsCount).isEqualTo(1) + } + + @Test + fun `test no last admin, 2 selected`() { + val sut = aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + listOf( + aSelectableSpaceRoom(isLastAdmin = false, isSelected = true), + aSelectableSpaceRoom(isLastAdmin = false, isSelected = true), + ).toPersistentList() + ) + ) + assertThat(sut.showQuickAction).isTrue() + assertThat(sut.areAllSelected).isTrue() + assertThat(sut.hasOnlyLastAdminRoom).isFalse() + assertThat(sut.selectedRoomsCount).isEqualTo(2) + } + + @Test + fun `test 1 last admin, 2 selected`() { + val sut = aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + listOf( + aSelectableSpaceRoom(isLastAdmin = true, isSelected = false), + aSelectableSpaceRoom(isLastAdmin = false, isSelected = true), + aSelectableSpaceRoom(isLastAdmin = false, isSelected = true), + ).toPersistentList() + ) + ) + assertThat(sut.showQuickAction).isTrue() + assertThat(sut.areAllSelected).isTrue() + assertThat(sut.hasOnlyLastAdminRoom).isFalse() + assertThat(sut.selectedRoomsCount).isEqualTo(2) + } + + @Test + fun `test only last admin`() { + val sut = aLeaveSpaceState( + selectableSpaceRooms = AsyncData.Success( + listOf( + aSelectableSpaceRoom(isLastAdmin = true, isSelected = false), + aSelectableSpaceRoom(isLastAdmin = true, isSelected = false), + ).toPersistentList() + ) + ) + assertThat(sut.showQuickAction).isFalse() + assertThat(sut.areAllSelected).isTrue() + assertThat(sut.hasOnlyLastAdminRoom).isTrue() + assertThat(sut.selectedRoomsCount).isEqualTo(0) + } +} diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt new file mode 100644 index 0000000000..731fdb85a6 --- /dev/null +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -0,0 +1,321 @@ +/* + * Copyright 2025 New 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.space.impl.root + +import com.google.common.truth.Truth.assertThat +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.acceptdecline.anAcceptDeclineInviteState +import io.element.android.features.invite.api.toInviteData +import io.element.android.features.invite.test.InMemorySeenInvitesStore +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +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.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.join.JoinRoom +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList +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.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom +import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList +import io.element.android.libraries.previewutils.room.aSpaceRoom +import io.element.android.tests.testutils.EventsRecorder +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.advanceUntilIdle +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import im.vector.app.features.analytics.plan.JoinedRoom as AnalyticsJoinedRoom + +class SpacePresenterTest { + @Test + fun `present - initial state`() = runTest { + val paginateResult = lambdaRecorder> { + Result.success(Unit) + } + val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult) + val presenter = createSpacePresenter(spaceRoomList = spaceRoomList) + presenter.test { + val state = awaitItem() + assertThat(state.currentSpace).isNull() + assertThat(state.children).isEmpty() + assertThat(state.seenSpaceInvites).isEmpty() + assertThat(state.hideInvitesAvatar).isFalse() + assertThat(state.hasMoreToLoad).isTrue() + assertThat(state.joinActions).isEmpty() + assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState()) + advanceUntilIdle() + paginateResult.assertions().isCalledOnce() + } + } + + @Test + fun `present - load more`() = runTest { + val paginateResult = lambdaRecorder> { + Result.success(Unit) + } + val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult) + val presenter = createSpacePresenter(spaceRoomList = spaceRoomList) + presenter.test { + val state = awaitItem() + advanceUntilIdle() + paginateResult.assertions().isCalledOnce() + state.eventSink(SpaceEvents.LoadMore) + advanceUntilIdle() + paginateResult.assertions().isCalledExactly(2) + } + } + + @Test + fun `present - has more to load value`() = runTest { + val paginateResult = lambdaRecorder> { + Result.success(Unit) + } + val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult) + val presenter = createSpacePresenter(spaceRoomList = spaceRoomList) + presenter.test { + val state = awaitItem() + advanceUntilIdle() + assertThat(state.hasMoreToLoad).isTrue() + spaceRoomList.emitPaginationStatus( + SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false) + ) + assertThat(awaitItem().hasMoreToLoad).isFalse() + spaceRoomList.emitPaginationStatus( + SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = true) + ) + assertThat(awaitItem().hasMoreToLoad).isTrue() + } + } + + @Test + fun `present - current space value`() = runTest { + val paginateResult = lambdaRecorder> { + Result.success(Unit) + } + val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult) + val presenter = createSpacePresenter(spaceRoomList = spaceRoomList) + presenter.test { + val state = awaitItem() + advanceUntilIdle() + assertThat(state.currentSpace).isNull() + val aSpace = aSpaceRoom() + spaceRoomList.emitCurrentSpace(aSpace) + assertThat(awaitItem().currentSpace).isEqualTo(aSpace) + } + } + + @Test + fun `present - children value`() = runTest { + val paginateResult = lambdaRecorder> { + Result.success(Unit) + } + val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult) + val presenter = createSpacePresenter(spaceRoomList = spaceRoomList) + presenter.test { + val state = awaitItem() + advanceUntilIdle() + assertThat(state.children).isEmpty() + val aSpace = aSpaceRoom() + spaceRoomList.emitSpaceRooms(listOf(aSpace)) + assertThat(awaitItem().children).containsExactly(aSpace) + } + } + + @Test + fun `present - join a room success`() = runTest { + val joinRoom = lambdaRecorder, AnalyticsJoinedRoom.Trigger, Result> { _, _, _ -> + Result.success(Unit) + } + val serverNames = listOf("via1", "via2") + val aNotJoinedRoom = aSpaceRoom( + roomId = A_ROOM_ID_2, + via = serverNames, + state = null, + ) + val fakeSpaceRoomList = FakeSpaceRoomList( + initialSpaceRoomsValue = listOf( + aSpaceRoom( + roomId = A_ROOM_ID, + state = CurrentUserMembership.JOINED, + ), + aNotJoinedRoom, + ), + paginateResult = { Result.success(Unit) }, + ) + val presenter = createSpacePresenter( + spaceRoomList = fakeSpaceRoomList, + joinRoom = FakeJoinRoom( + lambda = joinRoom, + ), + ) + presenter.test { + skipItems(1) + val state = awaitItem() + assertThat(state.joinActions[A_ROOM_ID_2]).isNull() + state.eventSink(SpaceEvents.Join(aNotJoinedRoom)) + val joiningState = awaitItem() + assertThat(joiningState.joinActions[A_ROOM_ID_2]).isEqualTo(AsyncAction.Loading) + // Let the joinRoom call complete + advanceUntilIdle() + runCurrent() + // The room is joined + fakeSpaceRoomList.emitSpaceRooms( + listOf( + aSpaceRoom( + roomId = A_ROOM_ID, + state = CurrentUserMembership.JOINED, + ), + aNotJoinedRoom.copy(state = CurrentUserMembership.JOINED), + ) + ) + skipItems(1) + val joinedState = awaitItem() + // Joined room is removed from the join actions + assertThat(joinedState.joinActions).doesNotContainKey(A_ROOM_ID_2) + joinRoom.assertions().isCalledOnce().with( + value(A_ROOM_ID_2.toRoomIdOrAlias()), + value(serverNames), + value(AnalyticsJoinedRoom.Trigger.SpaceHierarchy), + ) + } + } + + @Test + fun `present - join a room failure`() = runTest { + val aNotJoinedRoom = aSpaceRoom( + roomId = A_ROOM_ID_2, + state = null, + ) + val fakeSpaceRoomList = FakeSpaceRoomList( + initialSpaceRoomsValue = listOf( + aSpaceRoom( + roomId = A_ROOM_ID, + state = CurrentUserMembership.JOINED, + ), + aNotJoinedRoom, + ), + paginateResult = { Result.success(Unit) }, + ) + val presenter = createSpacePresenter( + spaceRoomList = fakeSpaceRoomList, + joinRoom = FakeJoinRoom( + lambda = { _, _, _ -> Result.failure(AN_EXCEPTION) }, + ), + ) + presenter.test { + skipItems(1) + val state = awaitItem() + assertThat(state.joinActions[A_ROOM_ID_2]).isNull() + state.eventSink(SpaceEvents.Join(aNotJoinedRoom)) + val joiningState = awaitItem() + assertThat(joiningState.joinActions[A_ROOM_ID_2]).isEqualTo(AsyncAction.Loading) + val errorState = awaitItem() + // Joined room is removed from the join actions + assertThat(errorState.joinActions[A_ROOM_ID_2]!!.isFailure()).isTrue() + // Clear error + errorState.eventSink(SpaceEvents.ClearFailures) + val clearedState = awaitItem() + assertThat(clearedState.joinActions[A_ROOM_ID_2]).isEqualTo(AsyncAction.Uninitialized) + } + } + + @Test + fun `present - accept invite is transmitted to acceptDeclineInviteState`() { + `invite action is transmitted to acceptDeclineInviteState`( + acceptInvite = true, + ) + } + + @Test + fun `present - decline invite is transmitted to acceptDeclineInviteState`() { + `invite action is transmitted to acceptDeclineInviteState`( + acceptInvite = false, + ) + } + + private fun `invite action is transmitted to acceptDeclineInviteState`( + acceptInvite: Boolean, + ) = runTest { + val eventRecorder = EventsRecorder() + val anInvitedRoom = aSpaceRoom( + roomId = A_ROOM_ID_2, + state = CurrentUserMembership.INVITED, + ) + val fakeSpaceRoomList = FakeSpaceRoomList( + initialSpaceRoomsValue = listOf( + aSpaceRoom( + roomId = A_ROOM_ID, + state = CurrentUserMembership.JOINED, + ), + anInvitedRoom, + ), + paginateResult = { Result.success(Unit) }, + ) + val presenter = createSpacePresenter( + spaceRoomList = fakeSpaceRoomList, + acceptDeclineInvitePresenter = { + anAcceptDeclineInviteState( + eventSink = eventRecorder, + ) + }, + ) + presenter.test { + skipItems(1) + val state = awaitItem() + assertThat(state.joinActions[A_ROOM_ID_2]).isNull() + if (acceptInvite) { + state.eventSink(SpaceEvents.AcceptInvite(anInvitedRoom)) + eventRecorder.assertSingle( + AcceptDeclineInviteEvents.AcceptInvite( + invite = anInvitedRoom.toInviteData(), + ) + ) + } else { + state.eventSink(SpaceEvents.DeclineInvite(anInvitedRoom)) + eventRecorder.assertSingle( + AcceptDeclineInviteEvents.DeclineInvite( + invite = anInvitedRoom.toInviteData(), + shouldConfirm = true, + blockUser = false, + ) + ) + } + } + } + + private fun TestScope.createSpacePresenter( + client: MatrixClient = FakeMatrixClient(), + spaceRoomList: SpaceRoomList = FakeSpaceRoomList(), + seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(), + joinRoom: JoinRoom = FakeJoinRoom( + lambda = { _, _, _ -> Result.success(Unit) }, + ), + acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, + ): SpacePresenter { + return SpacePresenter( + client = client, + spaceRoomList = spaceRoomList, + seenInvitesStore = seenInvitesStore, + joinRoom = joinRoom, + acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, + sessionCoroutineScope = backgroundScope, + ) + } +} diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt new file mode 100644 index 0000000000..d036d7023c --- /dev/null +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.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.space.impl.root + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncAction +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 org.junit.Test + +class SpaceStateTest { + @Test + fun `test default state`() { + val state = aSpaceState() + assertThat(state.hasAnyFailure).isFalse() + assertThat(state.isJoining(A_ROOM_ID)).isFalse() + } + + @Test + fun `test has failure`() { + val state = aSpaceState( + joinActions = mapOf( + A_ROOM_ID to AsyncAction.Uninitialized, + A_ROOM_ID_2 to AsyncAction.Failure(AN_EXCEPTION), + A_ROOM_ID_3 to AsyncAction.Success(Unit), + ) + ) + assertThat(state.hasAnyFailure).isTrue() + } + + @Test + fun `test isJoining`() { + val state = aSpaceState( + joinActions = mapOf( + A_ROOM_ID to AsyncAction.Loading, + ) + ) + assertThat(state.isJoining(A_ROOM_ID)).isTrue() + } +} diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt new file mode 100644 index 0000000000..f95b4e6514 --- /dev/null +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.root + +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +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.previewutils.room.aSpaceRoom +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 + +@RunWith(AndroidJUnit4::class) +class SpaceViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setSpaceView( + aSpaceState( + eventSink = eventsRecorder, + ), + onBackClick = it, + ) + rule.pressBack() + } + } + + @Test + fun `clicking on a room name invokes the expected callback`() { + val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, name = A_ROOM_NAME) + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnceWithParam(aSpaceRoom) { + rule.setSpaceView( + aSpaceState( + children = listOf(aSpaceRoom), + eventSink = eventsRecorder, + ), + onRoomClick = it, + ) + rule.onNodeWithText(A_ROOM_NAME).performClick() + } + } + + @Test + fun `clicking on Join room emits the expected Event`() { + val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = null) + val eventsRecorder = EventsRecorder() + rule.setSpaceView( + aSpaceState( + children = listOf(aSpaceRoom), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_join) + eventsRecorder.assertSingle(SpaceEvents.Join(aSpaceRoom)) + } + + @Test + fun `clicking on accept invite emits the expected Event`() { + val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED) + val eventsRecorder = EventsRecorder() + rule.setSpaceView( + aSpaceState( + children = listOf(aSpaceRoom), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_accept) + eventsRecorder.assertSingle(SpaceEvents.AcceptInvite(aSpaceRoom)) + } + + @Test + fun `clicking on decline invite emits the expected Event`() { + val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED) + val eventsRecorder = EventsRecorder() + rule.setSpaceView( + aSpaceState( + children = listOf(aSpaceRoom), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_decline) + eventsRecorder.assertSingle(SpaceEvents.DeclineInvite(aSpaceRoom)) + } +} + +private fun AndroidComposeTestRule.setSpaceView( + state: SpaceState, + onBackClick: () -> Unit = EnsureNeverCalled(), + onRoomClick: (SpaceRoom) -> Unit = EnsureNeverCalledWithParam(), + onShareSpace: () -> Unit = EnsureNeverCalled(), + onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(), + acceptDeclineInviteView: @Composable () -> Unit = {}, +) { + setContent { + SpaceView( + state = state, + onBackClick = onBackClick, + onRoomClick = onRoomClick, + onShareSpace = onShareSpace, + onLeaveSpaceClick = onLeaveSpaceClick, + acceptDeclineInviteView = acceptDeclineInviteView, + ) + } +} diff --git a/features/startchat/impl/build.gradle.kts b/features/startchat/impl/build.gradle.kts index 270c329a02..8ba7593b36 100644 --- a/features/startchat/impl/build.gradle.kts +++ b/features/startchat/impl/build.gradle.kts @@ -1,5 +1,5 @@ -import extension.ComponentMergingStrategy -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -23,7 +23,7 @@ android { } } -setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP) +setupDependencyInjection() dependencies { implementation(projects.libraries.core) @@ -33,7 +33,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) implementation(projects.libraries.androidutils) - implementation(projects.libraries.deeplink) + implementation(projects.libraries.deeplink.api) implementation(projects.libraries.mediapickers.api) implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.permissions.api) @@ -44,13 +44,7 @@ dependencies { implementation(projects.features.createroom.api) api(projects.features.startchat.api) - testImplementation(libs.test.junit) - testImplementation(libs.test.mockk) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs, true) testImplementation(projects.services.analytics.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.mediapickers.test) @@ -59,7 +53,4 @@ dependencies { testImplementation(projects.libraries.usersearch.test) testImplementation(projects.features.startchat.test) testImplementation(projects.libraries.featureflag.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartChatEntryPoint.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartChatEntryPoint.kt index 9f9073f1d7..c33ac4356f 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartChatEntryPoint.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartChatEntryPoint.kt @@ -10,14 +10,15 @@ package io.element.android.features.startchat.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.startchat.api.StartChatEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultStartChatEntryPoint @Inject constructor() : StartChatEntryPoint { +@Inject +class DefaultStartChatEntryPoint : StartChatEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): StartChatEntryPoint.NodeBuilder { val plugins = ArrayList() diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt index b09c5ea174..6847b1859d 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt @@ -8,7 +8,8 @@ package io.element.android.features.startchat.impl import androidx.compose.runtime.MutableState -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.CreatedRoom import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser import io.element.android.features.startchat.api.StartDMAction @@ -20,10 +21,10 @@ 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 @ContributesBinding(SessionScope::class) -class DefaultStartDMAction @Inject constructor( +@Inject +class DefaultStartDMAction( private val matrixClient: MatrixClient, private val analyticsService: AnalyticsService, ) : StartDMAction { diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/StartChatFlowNode.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/StartChatFlowNode.kt index a312b6fa84..30d1f3a2cf 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/StartChatFlowNode.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/StartChatFlowNode.kt @@ -18,9 +18,9 @@ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.startchat.DefaultStartChatNavigator import io.element.android.features.startchat.api.StartChatEntryPoint @@ -36,7 +36,8 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -class StartChatFlowNode @AssistedInject constructor( +@AssistedInject +class StartChatFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val createRoomEntryPoint: CreateRoomEntryPoint, diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressNode.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressNode.kt index 67dba8b46e..101958ddad 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressNode.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressNode.kt @@ -13,14 +13,15 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.startchat.StartChatNavigator import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class JoinRoomByAddressNode @AssistedInject constructor( +@AssistedInject +class JoinRoomByAddressNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: JoinRoomByAddressPresenter.Factory, diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressPresenter.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressPresenter.kt index 4e1f9f9ab0..540c1a4784 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressPresenter.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressPresenter.kt @@ -15,9 +15,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.startchat.StartChatNavigator import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.data.tryOrNull @@ -31,7 +31,8 @@ import kotlin.time.Duration.Companion.seconds private const val ADDRESS_RESOLVE_TIMEOUT_IN_SECONDS = 10 -class JoinRoomByAddressPresenter @AssistedInject constructor( +@AssistedInject +class JoinRoomByAddressPresenter( @Assisted private val navigator: StartChatNavigator, private val client: MatrixClient, private val roomAliasHelper: RoomAliasHelper, diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatNode.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatNode.kt index f42b7da7cc..9a9ca85160 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatNode.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatNode.kt @@ -16,18 +16,19 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.features.startchat.StartChatNavigator -import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase +import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(SessionScope::class) -class StartChatNode @AssistedInject constructor( +@AssistedInject +class StartChatNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: StartChatPresenter, diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt index 9f7bea5c68..10a9745f32 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import dev.zacsweers.metro.Inject import io.element.android.features.startchat.api.StartDMAction import io.element.android.features.startchat.impl.userlist.SelectionMode import io.element.android.features.startchat.impl.userlist.UserListDataStore @@ -27,9 +28,9 @@ 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 -import javax.inject.Inject -class StartChatPresenter @Inject constructor( +@Inject +class StartChatPresenter( presenterFactory: UserListPresenter.Factory, userRepository: UserRepository, userListDataStore: UserListDataStore, diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt index c964b18441..38d15f6de3 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt @@ -15,10 +15,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import com.squareup.anvil.annotations.ContributesBinding -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient @@ -31,7 +31,8 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -class DefaultUserListPresenter @AssistedInject constructor( +@AssistedInject +class DefaultUserListPresenter( @Assisted val args: UserListPresenterArgs, @Assisted val userRepository: UserRepository, @Assisted val userListDataStore: UserListDataStore, diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListDataStore.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListDataStore.kt index 64048d7e86..99fc19f6e1 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListDataStore.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListDataStore.kt @@ -7,12 +7,13 @@ package io.element.android.features.startchat.impl.userlist +import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import javax.inject.Inject -class UserListDataStore @Inject constructor() { +@Inject +class UserListDataStore { private val _selectedUsers: MutableStateFlow> = MutableStateFlow(emptyList()) fun selectUser(user: MatrixUser) { diff --git a/features/startchat/impl/src/main/res/values-bg/translations.xml b/features/startchat/impl/src/main/res/values-bg/translations.xml index 21ad117fb6..113ca0b71e 100644 --- a/features/startchat/impl/src/main/res/values-bg/translations.xml +++ b/features/startchat/impl/src/main/res/values-bg/translations.xml @@ -1,6 +1,7 @@ "Нова стая" + "Възникна грешка при опита за започване на чат" "Присъединяване към стая по адрес" "Не е валиден адрес" "Въведете…" diff --git a/features/startchat/impl/src/main/res/values-de/translations.xml b/features/startchat/impl/src/main/res/values-de/translations.xml index dff0a6fdea..7608517314 100644 --- a/features/startchat/impl/src/main/res/values-de/translations.xml +++ b/features/startchat/impl/src/main/res/values-de/translations.xml @@ -1,12 +1,12 @@ - "Neuer Raum" - "Raum-Verzeichnis" + "Neuer Chat" + "Chat-Verzeichnis" "Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten" - "Raum per Adresse betreten" + "Chat per Adresse beitreten" "Keine gültige Adresse" "Eintreten…" - "Passender Raum gefunden" - "Raum nicht gefunden" + "Passender Chat gefunden" + "Chat nicht gefunden" "z. B. #room -name:matrix.org" diff --git a/features/startchat/impl/src/main/res/values-ko/translations.xml b/features/startchat/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..2ea09bd2e6 --- /dev/null +++ b/features/startchat/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,12 @@ + + + "새 방" + "방 디렉토리" + "채팅을 시작하는 동안 오류가 발생했습니다." + "주소로 방에 참가하기" + "유효한 주소가 아닙니다" + "입력하다…" + "일치하는 방이 발견되었습니다" + "방을 찾을 수 없습니다" + "예: #room-name:matrix.org" + diff --git a/features/startchat/impl/src/main/res/values-pt-rBR/translations.xml b/features/startchat/impl/src/main/res/values-pt-rBR/translations.xml index f17991c56b..421ce1fed9 100644 --- a/features/startchat/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/startchat/impl/src/main/res/values-pt-rBR/translations.xml @@ -2,10 +2,10 @@ "Nova sala" "Diretório de salas" - "Ocorreu um erro ao tentar iniciar um chat" + "Ocorreu um erro ao tentar iniciar uma conversa" "Entrar na sala pelo endereço" "Não é um endereço válido" - "Entrar…" + "Digite…" "Foi encontrada uma sala correspondente" "Sala não encontrada" "Por exemplo, #nome-da-sala:matrix.org" diff --git a/features/startchat/impl/src/main/res/values-ro/translations.xml b/features/startchat/impl/src/main/res/values-ro/translations.xml index d306d83aab..315191a182 100644 --- a/features/startchat/impl/src/main/res/values-ro/translations.xml +++ b/features/startchat/impl/src/main/res/values-ro/translations.xml @@ -3,4 +3,10 @@ "Cameră nouă" "Director de camere" "A apărut o eroare la încercarea începerii conversației" + "Gasiți o cameră după adresă" + "Adresa nu e este validă" + "Introduceți…" + "S-a găsit o cameră" + "Nu a putut fi găsită nici o cameră" + "de exemplu #nume-camera:matrix.org" diff --git a/features/startchat/impl/src/main/res/values-tr/translations.xml b/features/startchat/impl/src/main/res/values-tr/translations.xml index 581996500d..9612a1656a 100644 --- a/features/startchat/impl/src/main/res/values-tr/translations.xml +++ b/features/startchat/impl/src/main/res/values-tr/translations.xml @@ -3,4 +3,9 @@ "Yeni oda" "Oda dizini" "Sohbet başlatmaya çalışırken bir hata oluştu" + "Bir adres ile odaya katılın" + "Geçerli bir adres değil" + "Eşleşen oda bulundu" + "Oda bulunamadı" + "örn. #room-isim:matrix.org" diff --git a/features/startchat/impl/src/main/res/values-uz/translations.xml b/features/startchat/impl/src/main/res/values-uz/translations.xml index 6f68899a3b..a789abc565 100644 --- a/features/startchat/impl/src/main/res/values-uz/translations.xml +++ b/features/startchat/impl/src/main/res/values-uz/translations.xml @@ -1,5 +1,6 @@ "Yangi xona" + "Xona katalogi" "Suhbatni boshlashda xatolik yuz berdi" diff --git a/features/startchat/impl/src/main/res/values-zh/translations.xml b/features/startchat/impl/src/main/res/values-zh/translations.xml index 2f80b65973..fcbb6afd45 100644 --- a/features/startchat/impl/src/main/res/values-zh/translations.xml +++ b/features/startchat/impl/src/main/res/values-zh/translations.xml @@ -6,6 +6,7 @@ "输入地址加入房间" "地址无效" "输入…" + "找到匹配的房间" "未找到房间" "例如 #room-name:matrix.org" diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartChatEntryPointTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartChatEntryPointTest.kt new file mode 100644 index 0000000000..8f4a41a3fa --- /dev/null +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartChatEntryPointTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.startchat.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.createroom.api.CreateRoomEntryPoint +import io.element.android.features.startchat.api.StartChatEntryPoint +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultStartChatEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultStartChatEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + StartChatFlowNode( + buildContext = buildContext, + plugins = plugins, + createRoomEntryPoint = object : CreateRoomEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + ) + } + val callback = object : StartChatEntryPoint.Callback { + override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List) = lambdaError() + override fun onOpenRoomDirectory() = lambdaError() + } + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .callback(callback) + .build() + assertThat(result).isInstanceOf(StartChatFlowNode::class.java) + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt index 59a8838c6b..7340981053 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt @@ -177,23 +177,23 @@ class StartChatPresenterTest { } } } - - private fun createStartChatPresenter( - startDMAction: StartDMAction = FakeStartDMAction(), - isRoomDirectorySearchEnabled: Boolean = false, - ): StartChatPresenter { - val featureFlagService = FakeFeatureFlagService( - initialState = mapOf( - FeatureFlags.RoomDirectorySearch.key to isRoomDirectorySearchEnabled, - ), - ) - return StartChatPresenter( - presenterFactory = FakeUserListPresenterFactory(FakeUserListPresenter()), - userRepository = FakeUserRepository(), - userListDataStore = UserListDataStore(), - startDMAction = startDMAction, - featureFlagService = featureFlagService, - buildMeta = aBuildMeta(), - ) - } +} + +internal fun createStartChatPresenter( + startDMAction: StartDMAction = FakeStartDMAction(), + isRoomDirectorySearchEnabled: Boolean = false, +): StartChatPresenter { + val featureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.RoomDirectorySearch.key to isRoomDirectorySearchEnabled, + ), + ) + return StartChatPresenter( + presenterFactory = FakeUserListPresenterFactory(FakeUserListPresenter()), + userRepository = FakeUserRepository(), + userListDataStore = UserListDataStore(), + startDMAction = startDMAction, + featureFlagService = featureFlagService, + buildMeta = aBuildMeta(), + ) } diff --git a/features/userprofile/impl/build.gradle.kts b/features/userprofile/impl/build.gradle.kts index 7bcebae565..f0c214c22e 100644 --- a/features/userprofile/impl/build.gradle.kts +++ b/features/userprofile/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2024 New Vector Ltd. @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.core) @@ -41,17 +42,8 @@ dependencies { implementation(projects.features.startchat.api) implementation(projects.services.analytics.api) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.test.mockk) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.startchat.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/DefaultUserProfileEntryPoint.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfileEntryPoint.kt index 07cd908304..bfc8a30df6 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfileEntryPoint.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfileEntryPoint.kt @@ -10,14 +10,15 @@ package io.element.android.features.userprofile.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.userprofile.api.UserProfileEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultUserProfileEntryPoint @Inject constructor() : UserProfileEntryPoint { +@Inject +class DefaultUserProfileEntryPoint : UserProfileEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): UserProfileEntryPoint.NodeBuilder { return object : UserProfileEntryPoint.NodeBuilder { val plugins = ArrayList() diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfilePresenterFactory.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfilePresenterFactory.kt index f95670d4fa..775cdf5ff9 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfilePresenterFactory.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfilePresenterFactory.kt @@ -7,17 +7,18 @@ package io.element.android.features.userprofile.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.userprofile.api.UserProfilePresenterFactory import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.impl.root.UserProfilePresenter import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.UserId -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultUserProfilePresenterFactory @Inject constructor( +@Inject +class DefaultUserProfilePresenterFactory( private val factory: UserProfilePresenter.Factory, ) : UserProfilePresenterFactory { override fun create(userId: UserId): Presenter = factory.create(userId) 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 e67a00a8b1..5828d60c25 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 @@ -17,9 +17,9 @@ import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.call.api.CallType import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.userprofile.api.UserProfileEntryPoint @@ -33,18 +33,19 @@ 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.SessionId 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 @ContributesNode(SessionScope::class) -class UserProfileFlowNode @AssistedInject constructor( +@AssistedInject +class UserProfileFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val elementCallEntryPoint: ElementCallEntryPoint, - private val sessionIdHolder: CurrentSessionIdHolder, + private val sessionId: SessionId, private val mediaViewerEntryPoint: MediaViewerEntryPoint, private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint, ) : BaseFlowNode( @@ -81,7 +82,7 @@ class UserProfileFlowNode @AssistedInject constructor( } override fun onStartCall(dmRoomId: RoomId) { - elementCallEntryPoint.startCall(CallType.RoomCall(sessionId = sessionIdHolder.current, roomId = dmRoomId)) + elementCallEntryPoint.startCall(CallType.RoomCall(sessionId = sessionId, roomId = dmRoomId)) } override fun onVerifyUser(userId: UserId) { @@ -98,7 +99,7 @@ class UserProfileFlowNode @AssistedInject constructor( } override fun onViewInTimeline(eventId: EventId) { - // Cannot happen + // Cannot happen } } mediaViewerEntryPoint.nodeBuilder(this, buildContext) 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 f10fd48a04..735957946a 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 @@ -14,10 +14,10 @@ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.features.userprofile.shared.UserProfileNodeHelper import io.element.android.features.userprofile.shared.UserProfileView import io.element.android.libraries.architecture.NodeInputs @@ -29,7 +29,8 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(SessionScope::class) -class UserProfileNode @AssistedInject constructor( +@AssistedInject +class UserProfileNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val analyticsService: AnalyticsService, 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 b7fae6082b..3f226d0213 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 @@ -17,9 +17,9 @@ import androidx.compose.runtime.produceState 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.startchat.api.StartDMAction import io.element.android.features.userprofile.api.UserProfileEvents @@ -41,7 +41,8 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -class UserProfilePresenter @AssistedInject constructor( +@AssistedInject +class UserProfilePresenter( @Assisted private val userId: UserId, private val client: MatrixClient, private val startDMAction: StartDMAction, diff --git a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfileEntryPointTest.kt b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfileEntryPointTest.kt new file mode 100644 index 0000000000..75bc434048 --- /dev/null +++ b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfileEntryPointTest.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.features.userprofile.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.ElementCallEntryPoint +import io.element.android.features.userprofile.api.UserProfileEntryPoint +import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint +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.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultUserProfileEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultUserProfileEntryPoint() + + val parentNode = TestParentNode.create { buildContext, plugins -> + UserProfileFlowNode( + buildContext = buildContext, + plugins = plugins, + sessionId = A_SESSION_ID, + elementCallEntryPoint = object : ElementCallEntryPoint { + override fun startCall(callType: CallType) = lambdaError() + override suspend fun handleIncomingCall( + callType: CallType.RoomCall, + eventId: EventId, + senderId: UserId, + roomName: String?, + senderName: String?, + avatarUrl: String?, + timestamp: Long, + expirationTimestamp: Long, + notificationChannelId: String, + textContent: String? + ) = lambdaError() + }, + mediaViewerEntryPoint = object : MediaViewerEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + outgoingVerificationEntryPoint = object : OutgoingVerificationEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, + ) + } + val callback = object : UserProfileEntryPoint.Callback { + override fun onOpenRoom(roomId: RoomId) { + lambdaError() + } + } + val params = UserProfileEntryPoint.Params( + userId = A_USER_ID, + ) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .callback(callback) + .build() + assertThat(result).isInstanceOf(UserProfileFlowNode::class.java) + assertThat(result.plugins).contains(params) + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/userprofile/shared/build.gradle.kts b/features/userprofile/shared/build.gradle.kts index 53e8e9dee2..95f64154cf 100644 --- a/features/userprofile/shared/build.gradle.kts +++ b/features/userprofile/shared/build.gradle.kts @@ -1,4 +1,4 @@ -import extension.setupAnvil +import extension.testCommonDependencies /* * Copyright 2024 New Vector Ltd. @@ -21,8 +21,6 @@ android { } } -setupAnvil() - dependencies { implementation(projects.libraries.core) implementation(projects.libraries.architecture) @@ -42,14 +40,6 @@ dependencies { implementation(projects.features.startchat.api) implementation(projects.services.analytics.api) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } 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 677034496c..b2e8611d3d 100644 --- a/features/userprofile/shared/src/main/res/values-bg/translations.xml +++ b/features/userprofile/shared/src/main/res/values-bg/translations.xml @@ -1,13 +1,18 @@ "Блокиране" + "Блокираните потребители няма да могат да ви изпращат съобщения и всички техни съобщения ще бъдат скрити. Можете да ги отблокирате по всяко време." "Блокиране на потребителя" "Отблокиране" + "Ще можете да виждате отново всички съобщения от тях." "Отблокиране на потребителя" "Блокиране" + "Блокираните потребители няма да могат да ви изпращат съобщения и всички техни съобщения ще бъдат скрити. Можете да ги отблокирате по всяко време." "Блокиране на потребителя" "Профил" "Отблокиране" + "Ще можете да виждате отново всички съобщения от тях." "Отблокиране на потребителя" "Потвърждаване на %1$s" + "Възникна грешка при опита за започване на чат" 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 b1c791af37..b3b7f1f7c2 100644 --- a/features/userprofile/shared/src/main/res/values-de/translations.xml +++ b/features/userprofile/shared/src/main/res/values-de/translations.xml @@ -13,7 +13,7 @@ "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 Nutzer zu verifizieren." - "Überprüfen Sie %1$s" + "Verwende die Web-App, um diesen Nutzer zu verifizieren." + "Verifiziere %1$s" "Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten" diff --git a/features/userprofile/shared/src/main/res/values-hu/translations.xml b/features/userprofile/shared/src/main/res/values-hu/translations.xml index 0a433ee5da..eeccf83f4c 100644 --- a/features/userprofile/shared/src/main/res/values-hu/translations.xml +++ b/features/userprofile/shared/src/main/res/values-hu/translations.xml @@ -4,15 +4,15 @@ "A letiltott felhasználók nem fognak tudni üzeneteket küldeni, és az összes üzenetük rejtve lesz. Bármikor feloldhatja a letiltásukat." "Felhasználó letiltása" "Letiltás feloldása" - "Újra láthatja az összes üzenetét." - "Felhasználó kitiltásának feloldása" + "Újra látni fogja az összes üzenetét." + "Felhasználó letiltásának feloldása" "Letiltás" "A letiltott felhasználók nem fognak tudni üzeneteket küldeni, és az összes üzenetük rejtve lesz. Bármikor feloldhatja a letiltásukat." "Felhasználó letiltása" "Profil" "Letiltás feloldása" - "Újra láthatja az összes üzenetét." - "Felhasználó kitiltásának feloldása" + "Újra látni fogja az összes üzenetét." + "Felhasználó letiltásának feloldása" "Használja a webes alkalmazást a felhasználó ellenőrzéséhez." "A(z) %1$s ellenőrzése" "Hiba történt a csevegés indításakor" diff --git a/features/userprofile/shared/src/main/res/values-ko/translations.xml b/features/userprofile/shared/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..a5518a9120 --- /dev/null +++ b/features/userprofile/shared/src/main/res/values-ko/translations.xml @@ -0,0 +1,19 @@ + + + "차단" + "차단된 사용자는 메시지를 보낼 수 없으며, 그들의 모든 메시지는 숨겨집니다. 언제든지 차단 해제할 수 있습니다." + "사용자 차단하기" + "차단 해제" + "그들로부터 보낸 모든 메시지를 다시 볼 수 있게 됩니다." + "사용자 차단 해제" + "차단" + "차단된 사용자는 메시지를 보낼 수 없으며, 그들의 모든 메시지는 숨겨집니다. 언제든지 차단 해제할 수 있습니다." + "사용자 차단하기" + "프로필" + "차단 해제" + "그들로부터 보낸 모든 메시지를 다시 볼 수 있게 됩니다." + "사용자 차단 해제" + "웹 앱을 사용하여 이 사용자를 확인하세요." + "확인 %1$s" + "채팅을 시작하는 동안 오류가 발생했습니다." + 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 e379aeb2ec..1f722c169d 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 @@ -4,16 +4,16 @@ "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" "Desbloquear" - "Você poderá ver todas as mensagens deles novamente." + "Você poderá ver todas as mensagens desta pessoa novamente." "Desbloquear usuário" "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." + "Você poderá ver todas as mensagens desta pessoa novamente." "Desbloquear usuário" - "Use o aplicativo da Web para verificar este usuário." + "Use o web app para verificar este usuário." "Verificar %1$s" - "Ocorreu um erro ao tentar iniciar um chat" + "Ocorreu um erro ao tentar iniciar uma conversa" diff --git a/features/userprofile/shared/src/main/res/values-pt/translations.xml b/features/userprofile/shared/src/main/res/values-pt/translations.xml index eae68be0b7..ec919a456a 100644 --- a/features/userprofile/shared/src/main/res/values-pt/translations.xml +++ b/features/userprofile/shared/src/main/res/values-pt/translations.xml @@ -14,6 +14,6 @@ "Poderás voltar a ver todas as suas mensagens." "Desbloquear utilizador" "Utiliza a aplicação Web para verificar este utilizador." - "Verifique %1$s" + "Verifica %1$s" "Ocorreu um erro ao tentar iniciar uma conversa" diff --git a/features/userprofile/shared/src/main/res/values-uz/translations.xml b/features/userprofile/shared/src/main/res/values-uz/translations.xml index 4e4fe08051..42edf2fb64 100644 --- a/features/userprofile/shared/src/main/res/values-uz/translations.xml +++ b/features/userprofile/shared/src/main/res/values-uz/translations.xml @@ -9,8 +9,11 @@ "Bloklash" "Bloklangan foydalanuvchilar sizga xabar yubora olmaydi va ularning barcha xabarlari yashiriladi. Ularni istalgan vaqtda blokdan chiqarishingiz mumkin." "Foydalanuvchini bloklash" + "Profil" "Blokdan chiqarish" "Ulardan kelgan barcha xabarlarni yana koʻrishingiz mumkin boʻladi." "Foydalanuvchini blokdan chiqarish" + "Bu foydalanuvchini tasdiqlash uchun veb-ilovadan foydalaning." + "Tasdiqlash %1$s" "Suhbatni boshlashda xatolik yuz berdi" diff --git a/features/verifysession/impl/build.gradle.kts b/features/verifysession/impl/build.gradle.kts index 6454c893e8..13e9010e7f 100644 --- a/features/verifysession/impl/build.gradle.kts +++ b/features/verifysession/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -20,7 +21,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.appconfig) @@ -37,17 +38,9 @@ dependencies { api(libs.statemachine) api(projects.features.verifysession.api) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs, true) testImplementation(projects.features.logout.test) testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.preferences.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/DefaultIncomingVerificationEntryPoint.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/DefaultIncomingVerificationEntryPoint.kt index c08319006c..c1a6418153 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/DefaultIncomingVerificationEntryPoint.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/DefaultIncomingVerificationEntryPoint.kt @@ -10,14 +10,15 @@ package io.element.android.features.verifysession.impl.incoming 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultIncomingVerificationEntryPoint @Inject constructor() : IncomingVerificationEntryPoint { +@Inject +class DefaultIncomingVerificationEntryPoint : IncomingVerificationEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): IncomingVerificationEntryPoint.NodeBuilder { val plugins = ArrayList() 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 802eadb13a..a17054b9e6 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 @@ -13,15 +13,16 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class IncomingVerificationNode @AssistedInject constructor( +@AssistedInject +class IncomingVerificationNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: IncomingVerificationPresenter.Factory, 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 66a2b2b2cd..176176a895 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 @@ -17,9 +17,9 @@ 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 -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.dateformatter.api.DateFormatter @@ -38,7 +38,8 @@ 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( +@AssistedInject +class IncomingVerificationPresenter( @Assisted private val verificationRequest: VerificationRequest.Incoming, @Assisted private val navigator: IncomingVerificationNavigator, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, @@ -47,7 +48,7 @@ class IncomingVerificationPresenter @AssistedInject constructor( private val dateFormatter: DateFormatter, ) : Presenter { @AssistedFactory - interface Factory { + fun interface Factory { fun create( verificationRequest: VerificationRequest.Incoming, navigator: IncomingVerificationNavigator, @@ -154,7 +155,7 @@ class IncomingVerificationPresenter @AssistedInject constructor( StateMachineState.RejectingIncomingVerification, null -> { Step.Initial( - deviceDisplayName = sessionVerificationRequestDetails.senderProfile.displayName ?: sessionVerificationRequestDetails.deviceId.value, + deviceDisplayName = sessionVerificationRequestDetails.deviceDisplayName, deviceId = sessionVerificationRequestDetails.deviceId, formattedSignInTime = formattedSignInTime, isWaiting = machineState == StateMachineState.AcceptingIncomingVerification || 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 fdc9f373d4..00bffa4ce3 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 @@ -22,7 +22,7 @@ data class IncomingVerificationState( @Stable sealed interface Step { data class Initial( - val deviceDisplayName: String, + val deviceDisplayName: String?, val deviceId: DeviceId, val formattedSignInTime: String, val isWaiting: Boolean, 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 9ff9f50cd8..40fbf4379c 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 @@ -10,15 +10,16 @@ package io.element.android.features.verifysession.impl.incoming import com.freeletics.flowredux.dsl.FlowReduxStateMachine +import dev.zacsweers.metro.Inject import io.element.android.features.verifysession.impl.util.andLogStateChange import io.element.android.features.verifysession.impl.util.logReceivedEvents import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.SessionVerificationService import kotlinx.coroutines.ExperimentalCoroutinesApi -import javax.inject.Inject import com.freeletics.flowredux.dsl.State as MachineState -class IncomingVerificationStateMachine @Inject constructor( +@Inject +class IncomingVerificationStateMachine( private val sessionVerificationService: SessionVerificationService, ) : FlowReduxStateMachine( initialState = State.Initial(isCancelled = false) 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 cdad2669d6..478e696889 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 @@ -14,6 +14,7 @@ import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificat 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.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails import io.element.android.libraries.matrix.api.verification.VerificationRequest @@ -55,26 +56,28 @@ internal fun aStepInitial( internal fun anIncomingSessionVerificationRequest() = VerificationRequest.Incoming.OtherSession( details = SessionVerificationRequestDetails( - senderProfile = SessionVerificationRequestDetails.SenderProfile( + senderProfile = MatrixUser( userId = UserId("@alice:example.com"), displayName = "Alice", avatarUrl = null, ), flowId = FlowId("1234"), deviceId = DeviceId("ILAKNDNASDLK"), + deviceDisplayName = "a device name", firstSeenTimestamp = 0, ) ) internal fun anIncomingUserVerificationRequest() = VerificationRequest.Incoming.User( details = SessionVerificationRequestDetails( - senderProfile = SessionVerificationRequestDetails.SenderProfile( + senderProfile = MatrixUser( userId = UserId("@alice:example.com"), displayName = "Alice", avatarUrl = null, ), flowId = FlowId("1234"), deviceId = DeviceId("ILAKNDNASDLK"), + deviceDisplayName = "a device name", firstSeenTimestamp = 0, ) ) 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 4d51b0ca4f..4506dfe0c9 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 @@ -26,6 +26,7 @@ import androidx.compose.ui.semantics.focused import androidx.compose.ui.semantics.progressBarRangeInfo import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -147,7 +148,7 @@ private fun IncomingVerificationHeader(step: Step, request: VerificationRequest. Step.Failure -> R.string.screen_session_verification_request_failure_subtitle } val timeLimitMessage = if (step.isTimeLimited) { - stringResource(CommonStrings.a11y_time_limited_action_required) + stringResource(CommonStrings.a11y_session_verification_time_limited_action_required) } else { "" } @@ -214,9 +215,7 @@ private fun ContentInitial( .padding(top = 24.dp), ) { VerificationUserProfileContent( - userId = request.details.senderProfile.userId, - displayName = request.details.senderProfile.displayName, - avatarUrl = request.details.senderProfile.avatarUrl, + user = request.details.senderProfile, ) } } @@ -238,7 +237,7 @@ private fun IncomingVerificationBottomMenu( VerificationBottomMenu { Button( modifier = Modifier.fillMaxWidth(), - text = stringResource(CommonStrings.action_start), + text = stringResource(CommonStrings.action_start_verification), onClick = { eventSink(IncomingVerificationViewEvents.StartVerification) }, ) TextButton( @@ -292,3 +291,11 @@ internal fun IncomingVerificationViewPreview(@PreviewParameter(IncomingVerificat state = state, ) } + +@Preview +@Composable +internal fun IncomingVerificationViewA11yPreview() = ElementPreview { + IncomingVerificationView( + state = anIncomingVerificationState(), + ) +} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/ui/SessionDetailsView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/ui/SessionDetailsView.kt index da3471ddc1..14cd6733d9 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/ui/SessionDetailsView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/ui/SessionDetailsView.kt @@ -34,7 +34,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun SessionDetailsView( - deviceName: String, + deviceName: String?, deviceId: DeviceId, signInFormattedTimestamp: String, modifier: Modifier = Modifier, @@ -61,7 +61,7 @@ fun SessionDetailsView( resourceId = CompoundDrawables.ic_compound_devices ) Text( - text = deviceName, + text = deviceName ?: deviceId.value, style = ElementTheme.typography.fontBodyMdMedium, color = ElementTheme.colors.textPrimary, ) @@ -87,9 +87,16 @@ fun SessionDetailsView( @PreviewsDayNight @Composable internal fun SessionDetailsViewPreview() = ElementPreview { - SessionDetailsView( - deviceName = "Element X Android", - deviceId = DeviceId("ILAKNDNASDLK"), - signInFormattedTimestamp = "12:34", - ) + Column { + SessionDetailsView( + deviceName = "Element X Android", + deviceId = DeviceId("ILAKNDNASDLK"), + signInFormattedTimestamp = "12:34", + ) + SessionDetailsView( + deviceName = null, + deviceId = DeviceId("ILAKNDNASDLK"), + signInFormattedTimestamp = "12:34", + ) + } } 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 index 8ab29ce9f2..8355e71f75 100644 --- 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 @@ -10,14 +10,15 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 { +@Inject +class DefaultOutgoingVerificationEntryPoint : OutgoingVerificationEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): OutgoingVerificationEntryPoint.NodeBuilder { val plugins = ArrayList() 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 index 4691a1d23f..9941ce58fe 100644 --- 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 @@ -13,15 +13,16 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.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( +@AssistedInject +class OutgoingVerificationNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: OutgoingVerificationPresenter.Factory, 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 index 8185681090..c985c15e36 100644 --- 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 @@ -16,9 +16,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.encryption.EncryptionService @@ -34,14 +34,15 @@ 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( +@AssistedInject +class OutgoingVerificationPresenter( @Assisted private val showDeviceVerifiedScreen: Boolean, @Assisted private val verificationRequest: VerificationRequest.Outgoing, private val sessionVerificationService: SessionVerificationService, private val encryptionService: EncryptionService, ) : Presenter { @AssistedFactory - interface Factory { + fun interface Factory { fun create( verificationRequest: VerificationRequest.Outgoing, showDeviceVerifiedScreen: Boolean, diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationView.kt index f58a5ac76a..2357b39be7 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationView.kt @@ -157,7 +157,10 @@ private fun OutgoingVerificationHeader(step: Step, request: VerificationRequest. } Step.Canceled -> CommonStrings.common_verification_failed Step.Ready -> R.string.screen_session_verification_compare_emojis_title - Step.Completed -> CommonStrings.common_verification_complete + Step.Completed -> when (request) { + is VerificationRequest.Outgoing.CurrentSession -> R.string.screen_session_verification_device_verified + is VerificationRequest.Outgoing.User -> 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 @@ -187,7 +190,7 @@ private fun OutgoingVerificationHeader(step: Step, request: VerificationRequest. is Step.Exit -> return } val timeLimitMessage = if (step.isTimeLimited) { - stringResource(CommonStrings.a11y_time_limited_action_required) + stringResource(CommonStrings.a11y_session_verification_time_limited_action_required) } else { "" } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt index 4f6cfcf456..2e17b736f2 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -23,7 +24,6 @@ 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 @@ -31,18 +31,21 @@ 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 +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 +/** + * Ref: https://www.figma.com/design/lMrKOhS8BEb75GXVq7FnNI/ER-96--User-Verification-by-Emoji?node-id=116-52049 + */ @Composable fun VerificationUserProfileContent( - userId: UserId, - displayName: String?, - avatarUrl: String?, + user: MatrixUser, modifier: Modifier = Modifier, ) { - val avatarData = remember(userId, displayName, avatarUrl) { - AvatarData(id = userId.value, name = displayName, url = avatarUrl, size = AvatarSize.UserVerification) + val avatarData = remember(user) { + user.getAvatarData(AvatarSize.UserVerification) } - Row( modifier = modifier .fillMaxWidth() @@ -55,12 +58,20 @@ fun VerificationUserProfileContent( avatarData = avatarData, avatarType = AvatarType.User, ) - Spacer(modifier = Modifier.padding(12.dp)) + Spacer(modifier = Modifier.width(12.dp)) Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text(text = displayName ?: userId.value, style = ElementTheme.typography.fontBodyLgMedium, color = ElementTheme.colors.textPrimary) + Text( + text = user.getBestName(), + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + ) - if (displayName != null) { - Text(text = userId.value, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textSecondary) + if (user.displayName.isNullOrEmpty().not()) { + Text( + text = user.userId.value, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + ) } } } @@ -72,8 +83,10 @@ internal fun VerificationUserProfileContentPreview() = ElementPreview( drawableFallbackForImages = CommonDrawables.sample_avatar ) { VerificationUserProfileContent( - userId = UserId("@alice:example.com"), - displayName = "Alice", - avatarUrl = "https://example.com/avatar.png", + user = MatrixUser( + userId = UserId("@alice:example.com"), + displayName = "Alice", + avatarUrl = "https://example.com/avatar.png", + ) ) } 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 ab0a15f4ed..ba1d05e84b 100644 --- a/features/verifysession/impl/src/main/res/values-bg/translations.xml +++ b/features/verifysession/impl/src/main/res/values-bg/translations.xml @@ -8,8 +8,9 @@ "Устройството е потвърдено" "Използване на друго устройство" "Нещо не изглежда наред. Или времето за изчакване на заявката е изтекло, или заявката е отхвърлена." - "Потвърдете, че емоджитата по-долу съвпадат с показаните в другата ви сесия." + "Потвърдете, че емоджитата по-долу съвпадат с показаните в другото ви устройство." "Сравнете емоджита" + "Сега можете да четете или изпращате съобщения сигурно на другото си устройство." "Въвеждане на ключ за възстановяване" "Докажете, че сте вие, за да получите достъп до хронологията на шифрованите си съобщения." "Отворете съществуваща сесия" @@ -17,6 +18,7 @@ "Готов съм" "В очакване на съвпадение" "Сравнете уникален набор от емоджита." + "Сравнете уникалните емоджита, като се уверите, че се появяват в същия ред." "Неуспешно потвърждаване" "Сега можете да четете или изпращате съобщения сигурно на другото си устройство." "Устройството е потвърдено" @@ -24,7 +26,7 @@ "Те съвпадат" "Уверете се, че приложението е отворено на другото устройство, преди да започнете потвърждението оттук." "Отворете приложението на друго потвърдено устройство" - "Чака се другото устройство" + "Започнете да потвърждавате на другото устройство" "Чака се другият потребител" "След като бъдете приети, ще можете да продължите потвърждението." "Приемете заявката, за да започнете процеса на потвърждаване в другата си сесия, за да продължите." 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 bdf73c53f1..5d0b28bc0a 100644 --- a/features/verifysession/impl/src/main/res/values-cs/translations.xml +++ b/features/verifysession/impl/src/main/res/values-cs/translations.xml @@ -11,13 +11,14 @@ "Použít jiné zařízení" "Čekání na jiném zařízení…" "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." + "Zkontrolujte, zda se níže uvedené emotikony shodují s emotikony zobrazenými na vašem druhém zařízení." "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 bezpečně číst nebo odesílat zprávy na svém druhém zařízení." "Nyní můžete důvěřovat identitě tohoto uživatele při odesílání nebo přijímání zpráv." + "Zařízení ověřeno" "Zadejte klíč pro obnovení" "Buď vypršel časový limit požadavku, požadavek byl zamítnut, nebo došlo k nesouladu ověření." "Pro přístup k historii zašifrovaných zpráv prokažte, že jste to vy." @@ -44,7 +45,7 @@ "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í" + "Spusťte ověření na druhém 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." diff --git a/features/verifysession/impl/src/main/res/values-da/translations.xml b/features/verifysession/impl/src/main/res/values-da/translations.xml index a1586feff7..c760b30ae3 100644 --- a/features/verifysession/impl/src/main/res/values-da/translations.xml +++ b/features/verifysession/impl/src/main/res/values-da/translations.xml @@ -11,13 +11,14 @@ "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." + "Bekræft, at emojierne nedenfor matcher dem, der vises på din anden enhed." "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 læse eller sende beskeder sikkert med din anden enhed." "Nu kan du stole på identiteten af denne bruger, når I sender og modtager beskeder fra hinanden." + "Enhed verificeret" "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." @@ -44,7 +45,7 @@ "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" + "Start verifikation 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." 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 5f47ad673e..acfbbaa0b1 100644 --- a/features/verifysession/impl/src/main/res/values-de/translations.xml +++ b/features/verifysession/impl/src/main/res/values-de/translations.xml @@ -2,52 +2,53 @@ "Bestätigung unmöglich?" "Erstelle einen neuen Wiederherstellungsschlüssel" - "Verifiziere dieses Gerät, um sicheres Messaging einzurichten." - "Bestätigen Sie Ihre Identität" + "Verifiziere dieses Gerät, um sichere Chats einzurichten." + "Bestätige deine 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." + "Du kannst jetzt verschlüsselte Nachrichten lesen und versenden. Dein Chatpartner vertraut nun diesem Gerät." "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." + "Bestätige, dass die folgenden Emojis mit denen auf deinem anderen Gerät ü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." + "Bestätige, dass die folgenden Emojis mit denen auf dem Gerät des anderen Nutzers übereinstimmen." + "Bestätige, dass die folgenden Zahlen mit denen in 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." + "Jetzt kannst du verschlüsselte Nachrichten sicher auf deinem anderen Gerät schreiben und lesen." + "Jetzt kannst du der Identität dieses Nutzers vertrauen, wenn du Nachrichten sendest oder empfängst." + "Gerät verifiziert" "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." + "Entweder ist die Anfrage abgelaufen, oder die Anfrage wurde abgelehnt, oder es gab eine Unstimmigkeit bei der Überprüfung." "Beweise deine Identität, um auf deinen verschlüsselten Nachrichtenverlauf zuzugreifen." - "Öffne eine bestehende Session" + "Öffne eine bestehende Sitzung" "Verifizierung wiederholen" "Ich bin bereit" "Warten auf eine Übereinstimmung" "Vergleiche eine spezielle Reihe von Emojis." "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." + "Entweder ist die Anfrage abgelaufen, oder die Anfrage wurde abgelehnt, oder es gab eine Unstimmigkeit bei der Überprüfung." "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." + "Fahre nur fort, falls du diese Verifizierung selbst gestartet hast." + "Verifiziere das andere Gerät, um deinen Nachrichtenverlauf sicher zu halten." + "Jetzt kannst du Nachrichten auf deinem anderen Gerät sicher lesen oder senden." "Gerät verifiziert" "Verifizierung angefordert" "Sie stimmen nicht überein" "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." + "Öffne die App auf dem anderen Gerät, bevor du die Verifizierung auf diesem Gerät startest." + "Öffne die App auf einem anderen verifizierten Gerät" + "Verifiziere diesen Nutzer für zusätzliche Sicherheit durch den Vergleich einer Reihe von Emojis auf den Geräten. Verwende dazu einen vertraulichen Kommunikationskanal." + "Diesen Nutzer verifizieren?" + "Für zusätzliche Sicherheit möchte ein anderer Nutzer deine Identität verifizieren. Es werden dir einige Emojis zum Vergleich angezeigt." + "Du solltest ein Popup-Fenster auf dem anderen Gerät sehen. Starte die Verifizierung von dort aus." + "Starte die Verifizierung auf dem anderen Gerät" + "Beginne die Verifizierung auf dem anderen Gerät" + "Warten auf den anderen Nutzer" + "Nach der Bestätigung kannst du mit der Verifizierung fortfahren." + "Akzeptiere die Anfrage für die Verifizierung in deiner anderen Sitzung um fortzufahren." "Warten auf die Annahme der Anfrage" "Abmelden…" diff --git a/features/verifysession/impl/src/main/res/values-eo/translations.xml b/features/verifysession/impl/src/main/res/values-eo/translations.xml new file mode 100644 index 0000000000..54822cd358 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-eo/translations.xml @@ -0,0 +1,16 @@ + + + "Create a new backup password" + "Confirm this device to set up secure messaging." + "Confirm it\'s you" + "Use backup password" + "Device confirmed" + "Confirm that the emojis below match those shown on your other session." + "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 this user when sending or receiving messages." + "Enter backup password" + "Device confirmed" + "Open the app on another confirmed device" + "For extra security, another user wants to verify you. You\'ll be shown a set of emojis to compare." + "Waiting for the other device" + 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 a675c879f2..473ab253fc 100644 --- a/features/verifysession/impl/src/main/res/values-fr/translations.xml +++ b/features/verifysession/impl/src/main/res/values-fr/translations.xml @@ -11,13 +11,14 @@ "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 émojis ci-dessous correspondent à ceux affichés sur votre autre session." + "Confirmez que les émojis ci-dessous correspondent à ceux affichés sur votre autre appareil." "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 lire ou envoyer des messages en toute sécurité sur votre autre appareil." "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." + "Appareil vérifié" "Utiliser la clé de récupération" "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." @@ -44,7 +45,7 @@ "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" + "Démarrer la vérification sur 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." 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 56abe18d67..a86a22fd1d 100644 --- a/features/verifysession/impl/src/main/res/values-hu/translations.xml +++ b/features/verifysession/impl/src/main/res/values-hu/translations.xml @@ -11,13 +11,14 @@ "Másik eszköz használata" "Várakozás a másik eszközre…" "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." + "Erősítse meg, hogy a lenti emodzsik megegyeznek a másik eszközön 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 biztonságosan olvashat vagy küldhet üzeneteket a másik eszközén." "Mostantól megbízhat a felhasználó személyazonosságában, amikor üzeneteket küld vagy fogad." + "Eszköz ellenőrizve" "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." @@ -44,7 +45,7 @@ "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" + "Ellenőrzés megkezdése a másik eszközön" "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." diff --git a/features/verifysession/impl/src/main/res/values-ko/translations.xml b/features/verifysession/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..f04ba0ba08 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,53 @@ + + + "확인할 수 없나요?" + "새로운 복구 키 만들기" + "보안 메시징을 설정하려면 이 장치를 확인하세요." + "본인 확인" + "다른 기기 사용" + "복구 키 사용" + "이제 메시지를 안전하게 읽거나 보낼 수 있으며, 채팅 상대도 이 기기를 신뢰할 수 있습니다." + "기기 검증됨" + "다른 기기 사용" + "다른 기기에서 대기 중…" + "무언가 잘못된 것 같습니다. 요청이 시간 초과되었거나 요청이 거부되었습니다." + "아래 이모티콘이 다른 세션에 표시된 이모티콘과 일치하는지 확인하세요." + "이모지 비교" + "아래 이모티콘이 다른 사용자의 기기에 표시된 이모티콘과 동일한지 확인하십시오." + "아래 숫자가 다른 세션에 표시된 숫자와 일치하는지 확인하세요." + "숫자 비교" + "새로운 세션이 확인되었습니다. 이 세션은 귀하의 암호화된 메시지에 액세스할 수 있으며, 다른 사용자는 이 세션을 신뢰할 수 있는 세션으로 인식합니다." + "이제 메시지를 보내거나 받을 때 이 사용자의 신원을 신뢰할 수 있습니다." + "복구 키를 입력하세요" + "요청이 시간 초과되었거나, 요청이 거부되었거나, 검증 불일치가 발생했습니다." + "암호화된 메시지 기록에 액세스하기 위해 본인임을 증명하세요." + "기존 세션 열기" + "검증 재시도" + "준비되었습니다" + "매칭을 기다리는 중…" + "고유한 이모지 세트를 비교하세요." + "고유한 이모지를 비교하여 동일한 순서로 표시되도록 확인하세요." + "로그인됨" + "요청이 시간 초과되었거나, 요청이 거부되었거나, 검증 불일치가 발생했습니다." + "검증 실패" + "본인이 이 검증을 시작한 경우에만 계속 진행하세요." + "다른 기기를 확인하여 메시지 기록을 안전하게 보호하세요." + "이제 다른 기기에서도 안전하게 메시지를 읽거나 보낼 수 있습니다." + "기기 검증됨" + "검증 요청" + "일치하지 않습니다" + "일치합니다" + "여기에서 검증을 시작하기 전에 다른 기기에서 앱이 실행되어 있는지 확인하십시오." + "다른 검증된 장치에서 앱을 실행하세요" + "보안을 강화하려면, 기기에 표시된 이모티콘을 비교하여 이 사용자를 확인하세요. 신뢰할 수 있는 통신 수단을 사용하여 확인하시기 바랍니다." + "이 사용자를 검증하시겠습니까?" + "추가 보안 위해 다른 사용자가 귀하의 신원을 확인하고자 합니다. 비교할 이모티콘 세트가 표시됩니다." + "다른 기기에 팝업이 표시될 것입니다. 지금 그곳에서 확인을 시작하세요." + "다른 장치에서 검증 시작" + "다른 기기를 기다리고 있습니다" + "다른 사용자를 기다리는 중" + "승인 후에는 검증 과정을 계속 진행할 수 있습니다." + "계속하려면 다른 세션에서 검증 과정을 시작하라는 요청을 수락하세요." + "요청 수락을 기다리는 중" + "로그아웃 중…" + 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 5ffb00abec..ad3b15869a 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 @@ -2,33 +2,33 @@ "Não consegue confirmar?" "Criar uma nova chave de recuperação" - "Verifique este dispositivo para configurar mensagens seguras." + "Verifique este dispositivo para configurar as mensagens seguras." "Confirme sua identidade" "Usar outro dispositivo" - "Use a chave de recuperação" + "Usar 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…" + "Aguardando o 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." + "Confirme se os emojis abaixo correspondem aos mostrados na 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." + "Confirme se os números abaixo correspondem aos mostrados na 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." + "Sua nova sessão está verificada agora. 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." + "Digitar chave de recuperação" + "Ou a solicitação expirou, a solicitação foi negada ou houve uma não correspondência na verificação." "Prove que é você para acessar seu histórico de mensagens criptografadas." "Abrir uma sessão existente" "Repetir verificação" "Estou pronto" - "Esperando para combinar" - "Compare um conjunto único de emojis." + "Aguardando a correspondência…" + "Compare um conjunto de emojis único." "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." + "Conectado" + "Ou a solicitação expirou, a solicitação foi negada ou houve uma não correspondência na 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." @@ -37,7 +37,7 @@ "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." + "Certifique-se de que você tenha o app aberto no outro dispositivo antes de iniciar a verificação por aqui." "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?" @@ -47,7 +47,7 @@ "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." + "Aceite a solicitação para iniciar o processo de verificação na 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 1386091254..b3de52b951 100644 --- a/features/verifysession/impl/src/main/res/values-pt/translations.xml +++ b/features/verifysession/impl/src/main/res/values-pt/translations.xml @@ -11,13 +11,14 @@ "Utilizar outro dispositivo" "A aguardar por outros dispositivos…" "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." + "Confirma que os emojis abaixo correspondem aos apresentados no teu outro dispositivo." "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 já podes ler ou enviar mensagens com segurança a partir do teu outro dispositivo." "Agora podes confiar na identidade deste utilizador quando envias ou recebes mensagens." + "Dispositivo verificado" "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." @@ -30,7 +31,7 @@ "Sessão iniciada" "O pedido expirou, o pedido foi recusado ou houve um erro de verificação." "A verificação falhou" - "Continue apenas se tiver iniciado esta verificação." + "Continua apenas se tiveres iniciado esta verificação." "Verifique o outro dispositivo para manter o histórico de mensagens seguro." "Agora podes ler ou enviar mensagens de forma segura no teu outro dispositivo." "Dispositivo verificado" @@ -44,7 +45,7 @@ "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" + "Inicia a verificação no teu 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." diff --git a/features/verifysession/impl/src/main/res/values-ro/translations.xml b/features/verifysession/impl/src/main/res/values-ro/translations.xml index ff6f1aaa62..08064b056a 100644 --- a/features/verifysession/impl/src/main/res/values-ro/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ro/translations.xml @@ -13,12 +13,14 @@ "Ceva nu este în regulă. Fie cererea a expirat, fie a fost respinsă." "Confirmați că emoticoanele de mai jos se potrivesc cu cele afișate în cealaltă sesiune." "Comparați emoticoanele" + "Confirmați că emoji-urile de mai jos corespund cu cele afișate pe dispozitivul celuilalt utilizator." "Confirmați că numerele de mai jos se potrivesc cu cele afișate în cealaltă sesiune." "Comparați numerele" "Noua dumneavoastră sesiune este acum verificată. Are acces la mesajele dumneavoastră criptate, iar alți utilizatori vă vor vedea ca fiind de încredere." + "Acum puteți avea încredere în identitatea acestui utilizator atunci când trimiteți sau primiți mesaje." "Introduceți cheia de recuperare" "Fie cererea a expirat, cererea a fost respinsă, fie a existat o nepotrivire de verificare." - "Demonstrați-vă identitatea pentru a accesa istoricul mesajelor criptate." + "Demonstrați-vă identitatea pentru a accesa mesaje anterioare criptate." "Deschideți o sesiune existentă" "Reîncercați verificarea" "Sunt pregătit" @@ -29,13 +31,23 @@ "Fie cererea a expirat, cererea a fost respinsă, fie a existat o nepotrivire de verificare." "Verificarea a eșuat" "Continuați numai dacă dumneavoastră ați inițiat această verificare." - "Verificați celălalt dispozitiv pentru a vă păstra istoricul mesajelor în siguranță." + "Verificați celălalt dispozitiv pentru a vă păstra mesajele anterioare în siguranță." "Acum puteți citi sau trimite mesaje în siguranță pe celălalt dispozitiv." "Dispozitiv verificat" - "Verificare solicitată" + "Verificare cerută" "Nu se potrivesc" "Se potrivesc" - "Acceptați solicitarea de a începe procesul de verificare în cealaltă sesiune pentru a continua." + "Asigurați-vă că aplicația este deschisă pe celălalt dispozitiv înainte de a începe verificarea de aici." + "Deschideți aplicația pe un alt dispozitiv verificat" + "Pentru securitate suplimentară, verificați acest utilizator comparând un set de emoji-uri pe dispozitivele dvs. Faceți acest lucru utilizând o metodă de comunicare de încredere." + "Verificați acest utilizator?" + "Pentru o securitate suplimentară, un alt utilizator dorește să vă verifice identitatea. Vi se va afișa un set de emoji-uri pentru comparație." + "Ar trebui să vedeți o fereastră pop-up pe celălalt dispozitiv. Începeți verificarea de acolo acum." + "Începeți verificarea pe celălalt dispozitiv" + "Se așteaptă celălalt dispozitiv" + "Se așteaptă celălalt utilizator" + "După acceptare, veți putea continua verificarea." + "Acceptați cererea de a începe procesul de verificare în cealaltă sesiune pentru a continua." "Se așteptă acceptarea cererii" "Deconectare în curs…" diff --git a/features/verifysession/impl/src/main/res/values-uz/translations.xml b/features/verifysession/impl/src/main/res/values-uz/translations.xml index 45596e1651..defdff1102 100644 --- a/features/verifysession/impl/src/main/res/values-uz/translations.xml +++ b/features/verifysession/impl/src/main/res/values-uz/translations.xml @@ -1,15 +1,38 @@ + "Tasdiqlay olmayapsizmi?" + "Yangi tiklash kalitini yarating" + "Xavfsiz xabarlashuvni sozlash uchun ushbu qurilmani tasdiqlang." + "Shaxsingizni tasdiqlang" + "Boshqa qurilmadan foydalanish" + "Qayta tiklash kalitidan foydalaning" + "Endi xabarlarni xavfsiz tarzda o‘qish yoki yuborish imkoniyatiga egasiz, shuningdek, siz bilan muloqot qilayotgan har qanday kishi ham bu qurilmaga ishonch bildirishi mumkin." + "Qurilma tasdiqlandi" + "Boshqa qurilmadan foydalanish" + "Boshqa qurilmada kutilmoqda…" "Nimadir noto‘g‘ri ko‘rinadi. Yoki so‘rov muddati tugadi yoki so‘rov rad etildi." "Quyidagi kulgichlar boshqa seansda ko‘rsatilganlarga mos kelishini tasdiqlang." "Emojilarni solishtiring" + "Quyidagi raqamlarning boshqa sessiyangizda koʻrsatilgan raqamlarga mos kelishini tasdiqlang." + "Sonlarni taqqoslash" "Yangi seansingiz tasdiqlandi. U sizning shifrlangan xabarlaringizga kirish huquqiga ega va boshqa foydalanuvchilar uni ishonchli deb bilishadi." + "Tiklash kalitini kiriting" + "So‘rov vaqti tugab qoldi, so‘rov rad etildi yoki tekshiruv mos kelmadi." "Shifrlangan xabarlar tarixiga kirish uchun shaxsingizni tasdiqlang." "Mavjud seansni oching" "Tasdiqlashni qaytadan urining" "Men tayyorman" "Mos kelishi kutilmoqda" + "Emojilarning noyob toʻplamini solishtiring." "Noyob emojilarni solishtiring, ular bir xil tartibda paydo bo\'lishiga ishonch hosil qiling." + "Tizimga kirildi" + "So‘rov vaqti tugab qoldi, so‘rov rad etildi yoki tekshiruv mos kelmadi." + "Tasdiqlanmadi" + "Bu tekshiruvni boshlagan bo‘lsangizgina davom eting." + "Xabarlaringiz tarixini xavfsiz saqlash uchun narigi qurilmani tasdiqlang." + "Endi xabarlarni boshqa qurilmangizda xavfsiz o‘qish yoki yuborishingiz mumkin." + "Qurilma tasdiqlandi" + "Tasdiqlash talab qilindi" "Ular mos kelmaydi" "Ular mos keladi" "Davom etish uchun boshqa seansda tekshirish jarayonini boshlash soʻrovini qabul qiling." 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 922cc8e193..113cda4a01 100644 --- a/features/verifysession/impl/src/main/res/values-zh/translations.xml +++ b/features/verifysession/impl/src/main/res/values-zh/translations.xml @@ -46,6 +46,7 @@ "在另一台设备上开始验证" "正在等待其他设备" "等待其他用户" + "一旦被接受,您将能够继续进行验证。" "请在其他会话中接受验证请求。" "等待接受请求" "正在登出…" diff --git a/features/verifysession/impl/src/main/res/values/localazy.xml b/features/verifysession/impl/src/main/res/values/localazy.xml index df9e6284d5..6bab676981 100644 --- a/features/verifysession/impl/src/main/res/values/localazy.xml +++ b/features/verifysession/impl/src/main/res/values/localazy.xml @@ -11,13 +11,14 @@ "Use another device" "Waiting on other device…" "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." + "Confirm that the emojis below match those shown on your other device." "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 read or send messages securely on your other device." "Now you can trust the identity of this user when sending or receiving messages." + "Device verified" "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." @@ -44,7 +45,7 @@ "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" + "Start verification on 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." diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/DefaultIncomingVerificationEntryPointTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/DefaultIncomingVerificationEntryPointTest.kt new file mode 100644 index 0000000000..ad586fc7fb --- /dev/null +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/DefaultIncomingVerificationEntryPointTest.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.verifysession.impl.incoming + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class DefaultIncomingVerificationEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() = runTest { + val entryPoint = DefaultIncomingVerificationEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + IncomingVerificationNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { _, _ -> createPresenter() } + ) + } + val callback = object : IncomingVerificationEntryPoint.Callback { + override fun onDone() = lambdaError() + } + val params = IncomingVerificationEntryPoint.Params( + verificationRequest = anIncomingSessionVerificationRequest() + ) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .callback(callback) + .build() + assertThat(result).isInstanceOf(IncomingVerificationNode::class.java) + assertThat(result.plugins).contains(params) + assertThat(result.plugins).contains(callback) + } +} 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 8fdf62df4f..2bf7116990 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 @@ -12,6 +12,7 @@ import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificat import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.matrix.api.core.FlowId +import io.element.android.libraries.matrix.api.user.MatrixUser 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 @@ -289,31 +290,32 @@ class IncomingVerificationPresenterTest { navigatorLambda.assertions().isCalledOnce() } } - - 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 TestScope.createPresenter( - verificationRequest: VerificationRequest.Incoming = anIncomingSessionVerificationRequest, - navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() }, - service: SessionVerificationService = FakeSessionVerificationService(), - dateFormatter: DateFormatter = FakeDateFormatter(), - ) = IncomingVerificationPresenter( - verificationRequest = verificationRequest, - navigator = navigator, - sessionVerificationService = service, - stateMachine = IncomingVerificationStateMachine(service), - dateFormatter = dateFormatter, - sessionCoroutineScope = backgroundScope, - ) } + +private val anIncomingSessionVerificationRequest = VerificationRequest.Incoming.OtherSession( + details = SessionVerificationRequestDetails( + senderProfile = MatrixUser( + userId = A_USER_ID, + displayName = "a user name", + avatarUrl = null, + ), + flowId = FlowId("flowId"), + deviceId = A_DEVICE_ID, + deviceDisplayName = "a device name", + firstSeenTimestamp = A_TIMESTAMP, + ) +) + +internal fun TestScope.createPresenter( + verificationRequest: VerificationRequest.Incoming = anIncomingSessionVerificationRequest, + navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() }, + service: SessionVerificationService = FakeSessionVerificationService(), + dateFormatter: DateFormatter = FakeDateFormatter(), +) = IncomingVerificationPresenter( + 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/incoming/IncomingVerificationViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt index 1e1c629a66..6b61e05689 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt @@ -62,7 +62,7 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_start) + rule.clickOn(CommonStrings.action_start_verification) eventsRecorder.assertSingle(IncomingVerificationViewEvents.StartVerification) } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/DefaultOutgoingVerificationEntryPointTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/DefaultOutgoingVerificationEntryPointTest.kt new file mode 100644 index 0000000000..52ff36dbd6 --- /dev/null +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/DefaultOutgoingVerificationEntryPointTest.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.android.features.verifysession.impl.outgoing + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultOutgoingVerificationEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultOutgoingVerificationEntryPoint() + + val parentNode = TestParentNode.create { buildContext, plugins -> + OutgoingVerificationNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { _, _ -> + createOutgoingVerificationPresenter() + } + ) + } + val callback = object : OutgoingVerificationEntryPoint.Callback { + override fun onLearnMoreAboutEncryption() = lambdaError() + override fun onBack() = lambdaError() + override fun onDone() = lambdaError() + } + val params = OutgoingVerificationEntryPoint.Params( + showDeviceVerifiedScreen = true, + verificationRequest = anOutgoingSessionVerificationRequest(), + ) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .callback(callback) + .build() + assertThat(result).isInstanceOf(OutgoingVerificationNode::class.java) + assertThat(result.plugins).contains(params) + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt index 06940f37a5..2eac19d018 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt @@ -321,18 +321,18 @@ class OutgoingVerificationPresenterTest { 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, - ) - } +} + +internal fun createOutgoingVerificationPresenter( + service: SessionVerificationService = FakeSessionVerificationService(), + 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/viewfolder/impl/build.gradle.kts b/features/viewfolder/impl/build.gradle.kts index 015e23a85f..0c7ef17b22 100644 --- a/features/viewfolder/impl/build.gradle.kts +++ b/features/viewfolder/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2024 New Vector Ltd. @@ -16,7 +17,7 @@ android { namespace = "io.element.android.features.viewfolder.impl" } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.androidutils) @@ -26,12 +27,6 @@ dependencies { implementation(projects.libraries.uiStrings) api(projects.features.viewfolder.api) - testImplementation(libs.test.junit) - testImplementation(libs.test.robolectric) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(projects.tests.testutils) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) } 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 index 55743f54cc..0e1f1e11b9 100644 --- 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 @@ -9,16 +9,17 @@ package io.element.android.features.viewfolder.impl import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 { +@Inject +class DefaultTextFileViewer : TextFileViewer { @Composable override fun Render( lines: ImmutableList, diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/DefaultViewFolderEntryPoint.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/DefaultViewFolderEntryPoint.kt index 47aaabfac6..330ca68a8c 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/DefaultViewFolderEntryPoint.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/DefaultViewFolderEntryPoint.kt @@ -10,21 +10,22 @@ package io.element.android.features.viewfolder.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.viewfolder.api.ViewFolderEntryPoint -import io.element.android.features.viewfolder.impl.root.ViewFolderRootNode +import io.element.android.features.viewfolder.impl.root.ViewFolderFlowNode import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultViewFolderEntryPoint @Inject constructor() : ViewFolderEntryPoint { +@Inject +class DefaultViewFolderEntryPoint : ViewFolderEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): ViewFolderEntryPoint.NodeBuilder { val plugins = ArrayList() return object : ViewFolderEntryPoint.NodeBuilder { override fun params(params: ViewFolderEntryPoint.Params): ViewFolderEntryPoint.NodeBuilder { - plugins += ViewFolderRootNode.Inputs(params.rootPath) + plugins += ViewFolderFlowNode.Inputs(params.rootPath) return this } @@ -34,7 +35,7 @@ class DefaultViewFolderEntryPoint @Inject constructor() : ViewFolderEntryPoint { } override fun build(): Node { - return parentNode.createNode(buildContext, plugins) + return parentNode.createNode(buildContext, plugins) } } } 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 e94bde4a53..e3635e40f4 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 @@ -7,20 +7,21 @@ package io.element.android.features.viewfolder.impl.file -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 -import javax.inject.Inject interface FileContentReader { suspend fun getLines(path: String): Result> } @ContributesBinding(AppScope::class) -class DefaultFileContentReader @Inject constructor( +@Inject +class DefaultFileContentReader( private val dispatchers: CoroutineDispatchers, ) : FileContentReader { override suspend fun getLines(path: String): Result> = withContext(dispatchers.io) { 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 78648c6738..9323281471 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 @@ -13,18 +13,18 @@ import android.os.Build import android.os.Environment import android.provider.MediaStore import androidx.annotation.RequiresApi -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 +import io.element.android.libraries.di.annotations.ApplicationContext import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File import java.io.FileOutputStream -import javax.inject.Inject interface FileSave { suspend fun save( @@ -33,7 +33,8 @@ interface FileSave { } @ContributesBinding(AppScope::class) -class DefaultFileSave @Inject constructor( +@Inject +class DefaultFileSave( @ApplicationContext private val context: Context, private val dispatchers: CoroutineDispatchers, ) : FileSave { 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 471f2df5d4..3c8aae4f39 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 @@ -11,17 +11,17 @@ import android.content.Context import android.content.Intent import android.net.Uri import androidx.core.content.FileProvider -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.ApplicationContext import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File -import javax.inject.Inject interface FileShare { suspend fun share( @@ -30,7 +30,8 @@ interface FileShare { } @ContributesBinding(AppScope::class) -class DefaultFileShare @Inject constructor( +@Inject +class DefaultFileShare( @ApplicationContext private val context: Context, private val dispatchers: CoroutineDispatchers, private val buildMeta: BuildMeta, diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileNode.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileNode.kt index 242fedebf5..41369dda07 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileNode.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileNode.kt @@ -13,15 +13,16 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class ViewFileNode @AssistedInject constructor( +@AssistedInject +class ViewFileNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: ViewFilePresenter.Factory, diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFilePresenter.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFilePresenter.kt index 12cbeda65c..fb330327f3 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFilePresenter.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFilePresenter.kt @@ -14,15 +14,16 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class ViewFilePresenter @AssistedInject constructor( +@AssistedInject +class ViewFilePresenter( @Assisted("path") val path: String, @Assisted("name") val name: String, private val fileContentReader: FileContentReader, diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/FolderExplorer.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/FolderExplorer.kt index 403a0ac49c..dec0541622 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/FolderExplorer.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/FolderExplorer.kt @@ -7,21 +7,22 @@ package io.element.android.features.viewfolder.impl.folder -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.features.viewfolder.impl.model.Item import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.di.AppScope import kotlinx.coroutines.withContext import java.io.File -import javax.inject.Inject interface FolderExplorer { suspend fun getItems(path: String): List } @ContributesBinding(AppScope::class) -class DefaultFolderExplorer @Inject constructor( +@Inject +class DefaultFolderExplorer( private val fileSizeFormatter: FileSizeFormatter, private val dispatchers: CoroutineDispatchers, ) : FolderExplorer { diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderNode.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderNode.kt index 600baf862d..4c57ea4135 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderNode.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderNode.kt @@ -13,16 +13,17 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.viewfolder.impl.model.Item import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class ViewFolderNode @AssistedInject constructor( +@AssistedInject +class ViewFolderNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: ViewFolderPresenter.Factory, diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderPresenter.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderPresenter.kt index c303b0e612..cac5c6ab66 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderPresenter.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderPresenter.kt @@ -13,17 +13,21 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.viewfolder.impl.model.Item import io.element.android.libraries.architecture.Presenter -import kotlinx.collections.immutable.toImmutableList +import io.element.android.libraries.core.meta.BuildMeta +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList -class ViewFolderPresenter @AssistedInject constructor( +@AssistedInject +class ViewFolderPresenter( @Assisted val canGoUp: Boolean, @Assisted val path: String, private val folderExplorer: FolderExplorer, + private val buildMeta: BuildMeta, ) : Presenter { @AssistedFactory interface Factory { @@ -32,16 +36,24 @@ class ViewFolderPresenter @AssistedInject constructor( @Composable override fun present(): ViewFolderState { - var content by remember { mutableStateOf(emptyList()) } + var content by remember { mutableStateOf(persistentListOf()) } + val title = remember { + buildString { + if (path.contains(buildMeta.applicationId)) { + append("…") + } + append(path.substringAfter(buildMeta.applicationId)) + } + } LaunchedEffect(Unit) { content = buildList { if (canGoUp) add(Item.Parent) addAll(folderExplorer.getItems(path)) - } + }.toPersistentList() } return ViewFolderState( - path = path, - content = content.toImmutableList(), + title = title, + content = content, ) } } diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderState.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderState.kt index c1fa5bfa01..3eb48790a4 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderState.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderState.kt @@ -11,6 +11,6 @@ import io.element.android.features.viewfolder.impl.model.Item import kotlinx.collections.immutable.ImmutableList data class ViewFolderState( - val path: String, + val title: String, val content: ImmutableList, ) diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderStateProvider.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderStateProvider.kt index 373601339d..cbaac89cc2 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderStateProvider.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderStateProvider.kt @@ -26,9 +26,9 @@ open class ViewFolderStateProvider : PreviewParameterProvider { } fun aViewFolderState( - path: String = "aPath", + title: String = "aPath", content: List = emptyList(), ) = ViewFolderState( - path = path, + title = title, content = content.toImmutableList(), ) 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 99de3badb8..bf5360f847 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 @@ -53,7 +53,7 @@ fun ViewFolderView( navigationIcon = { BackButton(onClick = onBackClick) }, - titleStr = state.path, + titleStr = state.title, ) }, content = { padding -> diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/root/ViewFolderRootNode.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/root/ViewFolderFlowNode.kt similarity index 94% rename from features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/root/ViewFolderRootNode.kt rename to features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/root/ViewFolderFlowNode.kt index 81084ead2a..d57824f2fd 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/root/ViewFolderRootNode.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/root/ViewFolderFlowNode.kt @@ -17,9 +17,10 @@ import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.features.viewfolder.api.ViewFolderEntryPoint import io.element.android.features.viewfolder.impl.file.ViewFileNode import io.element.android.features.viewfolder.impl.folder.ViewFolderNode @@ -29,14 +30,14 @@ import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.AppScope import kotlinx.parcelize.Parcelize @ContributesNode(AppScope::class) -class ViewFolderRootNode @AssistedInject constructor( +@AssistedInject +class ViewFolderFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, -) : BaseFlowNode( +) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap, diff --git a/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/impl/DefaultViewFolderEntryPointTest.kt b/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/impl/DefaultViewFolderEntryPointTest.kt new file mode 100644 index 0000000000..47d5992d8c --- /dev/null +++ b/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/impl/DefaultViewFolderEntryPointTest.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.viewfolder.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.viewfolder.api.ViewFolderEntryPoint +import io.element.android.features.viewfolder.impl.root.ViewFolderFlowNode +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultViewFolderEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultViewFolderEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + ViewFolderFlowNode( + buildContext = buildContext, + plugins = plugins, + ) + } + val callback = object : ViewFolderEntryPoint.Callback { + override fun onDone() = lambdaError() + } + val params = ViewFolderEntryPoint.Params( + rootPath = "path", + ) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .callback(callback) + .build() + assertThat(result).isInstanceOf(ViewFolderFlowNode::class.java) + assertThat(result.plugins).contains(ViewFolderFlowNode.Inputs(params.rootPath)) + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/test/folder/ViewFolderPresenterTest.kt b/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/test/folder/ViewFolderPresenterTest.kt index 542525ee3f..715dfa711f 100644 --- a/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/test/folder/ViewFolderPresenterTest.kt +++ b/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/test/folder/ViewFolderPresenterTest.kt @@ -14,7 +14,10 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.viewfolder.impl.folder.FolderExplorer import io.element.android.features.viewfolder.impl.folder.ViewFolderPresenter import io.element.android.features.viewfolder.impl.model.Item +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.test.core.aBuildMeta 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 @@ -30,11 +33,25 @@ class ViewFolderPresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.path).isEqualTo("aPath") + assertThat(initialState.title).isEqualTo("aPath") assertThat(initialState.content).isEmpty() } } + @Test + fun `present - title is built regarding the applicationId`() = runTest { + val presenter = createPresenter( + path = "/data/user/O/appId/cache/logs", + buildMeta = aBuildMeta( + applicationId = "appId", + ) + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.title).isEqualTo("…/cache/logs") + } + } + @Test fun `present - list items from root`() = runTest { val items = listOf( @@ -50,7 +67,7 @@ class ViewFolderPresenterTest { }.test { skipItems(1) val initialState = awaitItem() - assertThat(initialState.path).isEqualTo("aPath") + assertThat(initialState.title).isEqualTo("aPath") assertThat(initialState.content.toList()).isEqualTo(items) } } @@ -73,7 +90,7 @@ class ViewFolderPresenterTest { }.test { skipItems(1) val initialState = awaitItem() - assertThat(initialState.path).isEqualTo("aPath") + assertThat(initialState.title).isEqualTo("aPath") assertThat(initialState.content.toList()).isEqualTo(listOf(Item.Parent) + items) } } @@ -82,9 +99,13 @@ class ViewFolderPresenterTest { canGoUp: Boolean = false, path: String = "aPath", folderExplorer: FolderExplorer = FakeFolderExplorer(), + buildMeta: BuildMeta = aBuildMeta( + applicationId = "appId", + ), ) = ViewFolderPresenter( path = path, canGoUp = canGoUp, folderExplorer = folderExplorer, + buildMeta = buildMeta, ) } diff --git a/gradle.properties b/gradle.properties index 1c4ccb38eb..18399ccf06 100644 --- a/gradle.properties +++ b/gradle.properties @@ -41,7 +41,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.12.1 +# android.experimental.lint.version=8.12.2 # Enable test fixture for all modules by default android.experimental.enableTestFixtures=true @@ -49,8 +49,8 @@ android.experimental.enableTestFixtures=true # Create BuildConfig files as bytecode to avoid Java compilation phase android.enableBuildConfigAsBytecode=true -# Add the KSP code generation annotations to the list of contributing annotations for Anvil -com.squareup.anvil.kspContributingAnnotations=io.element.android.anvilannotations.ContributesNode - # Only apply KSP to main sources ksp.allow.all.target.configuration=false + +# Used to prevent detekt from reusing invalid cached rules +detekt.use.worker.api=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a0b6019eba..ba09935e55 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,11 +3,12 @@ [versions] # Project -android_gradle_plugin = "8.12.1" +# We cannot use 8.12.+ since it breaks F-Droid build (see https://github.com/element-hq/element-x-android/issues/3420#issuecomment-3199571010) +android_gradle_plugin = "8.11.1" # When updateing this, please also update the version in the file ./idea/kotlinc.xml -kotlin = "2.2.10" +kotlin = "2.2.20" kotlinpoet = "2.2.0" -ksp = "2.2.10-2.0.2" +ksp = "2.2.20-2.0.2" firebaseAppDistribution = "5.1.1" # AndroidX @@ -16,9 +17,9 @@ datastore = "1.1.7" constraintlayout = "2.2.1" constraintlayout_compose = "1.1.1" lifecycle = "2.9.2" -activity = "1.10.1" +activity = "1.11.0" media3 = "1.8.0" -camera = "1.4.2" +camera = "1.5.0" # Compose compose_bom = "2025.07.00" @@ -43,15 +44,14 @@ showkase = "1.0.5" appyx = "1.7.1" sqldelight = "2.1.0" wysiwyg = "2.39.0" -telephoto = "0.16.0" +telephoto = "0.17.0" haze = "1.6.10" # Dependency analysis -dependencyAnalysis = "2.19.0" +dependencyAnalysis = "3.0.4" # DI -dagger = "2.57" -anvil = "0.4.1" +metro = "0.6.8" # Auto service autoservice = "1.1.1" @@ -66,15 +66,15 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref compose_compiler_plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } # https://developer.android.com/studio/write/java8-support#library-desugaring-versions android_desugar = "com.android.tools:desugar_jdk_libs:2.1.5" -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" } +metro_gradle_plugin = { module = "dev.zacsweers.metro:gradle-plugin", version.ref = "metro" } +kotlin_compiler = { module = "org.jetbrains.kotlin:kotlin-compiler", 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.3" # https://firebase.google.com/docs/android/setup#available-libraries -google_firebase_bom = "com.google.firebase:firebase-bom:34.1.0" +google_firebase_bom = "com.google.firebase:firebase-bom:34.3.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" } @@ -152,14 +152,22 @@ test_runner = "androidx.test:runner:1.7.0" test_mockk = "io.mockk:mockk:1.14.5" test_konsist = "com.lemonappdev:konsist:0.17.3" test_turbine = "app.cash.turbine:turbine:1.2.1" -test_truth = "com.google.truth:truth:1.4.4" -test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.18" +test_truth = "com.google.truth:truth:1.4.5" +test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.19" test_robolectric = "org.robolectric:robolectric:4.15.1" test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.7.0" test_detekt_api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" } test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" } +# Matrix SDK +# When upgrading the library, you may want to check what's new in the FFI layer by having a look to the +# latest commits from the history of this file: +# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt +# All new features should not be implemented in the pull request that upgrades the version, developers should +# only fix API breaks and may add some TODOs. +matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.10.1" + # Others coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } coil_network_okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } @@ -172,24 +180,23 @@ serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-jso kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0" showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" } showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" } -jsoup = "org.jsoup:jsoup:1.21.1" +jsoup = "org.jsoup:jsoup:1.21.2" appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } -molecule-runtime = "app.cash.molecule:molecule-runtime:2.1.0" +molecule-runtime = "app.cash.molecule:molecule-runtime:2.2.0" timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.8.18" 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:sqlcipher-android:4.10.0" -sqlite = "androidx.sqlite:sqlite-ktx:2.5.2" +sqlite = "androidx.sqlite:sqlite-ktx:2.6.1" unifiedpush = "org.unifiedpush.android:connector:3.0.10" 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.13.0" +maplibre = "org.maplibre.gl:android-sdk:11.13.5" 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.2.0" @@ -198,24 +205,20 @@ 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.20.2" -sentry = "io.sentry:sentry-android:8.19.1" +posthog = "com.posthog:posthog-android:3.22.0" +sentry = "io.sentry:sentry-android:8.22.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.4.2" +matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.4.3" sigpwned_emoji4j = "com.sigpwned:emoji4j-core:16.0.0" # Di -inject = "javax.inject:javax.inject:1" -dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } -dagger_compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } -anvil_compiler_api = { module = "dev.zacsweers.anvil:compiler-api", version.ref = "anvil" } -anvil_compiler_utils = { module = "dev.zacsweers.anvil:compiler-utils", version.ref = "anvil" } +metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" } # Element Call -element_call_embedded = "io.element.android:element-call-embedded:0.14.1" +element_call_embedded = "io.element.android:element-call-embedded:0.16.0" # Auto services google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } @@ -232,18 +235,19 @@ android_library = { id = "com.android.library", version.ref = "android_gradle_pl kotlin_android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin_jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin_serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -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" } +# Note: used in DependencyInjectionExtensions.kt +metro = { id = "dev.zacsweers.metro", version.ref = "metro" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } -ktlint = "org.jlleitschuh.gradle.ktlint:13.0.0" +ktlint = "org.jlleitschuh.gradle.ktlint:13.1.0" dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12" -dependencycheck = "org.owasp.dependencycheck:12.1.3" +dependencycheck = "org.owasp.dependencycheck:12.1.6" dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" } paparazzi = "app.cash.paparazzi:2.0.0-alpha02" 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.2.0.5505" +sonarqube = "org.sonarqube:6.3.1.5724" licensee = "app.cash.licensee:1.13.0" compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +gms_google_services = { id = "com.google.gms.google-services", version = "4.4.3" } diff --git a/libraries/accountselect/api/build.gradle.kts b/libraries/accountselect/api/build.gradle.kts new file mode 100644 index 0000000000..7e0ce303f9 --- /dev/null +++ b/libraries/accountselect/api/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.libraries.accountselect.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) +} diff --git a/libraries/accountselect/api/src/main/kotlin/io/element/android/libraries/accountselect/api/AccountSelectEntryPoint.kt b/libraries/accountselect/api/src/main/kotlin/io/element/android/libraries/accountselect/api/AccountSelectEntryPoint.kt new file mode 100644 index 0000000000..72da3491de --- /dev/null +++ b/libraries/accountselect/api/src/main/kotlin/io/element/android/libraries/accountselect/api/AccountSelectEntryPoint.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.accountselect.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.SessionId + +interface AccountSelectEntryPoint : FeatureEntryPoint { + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onSelectAccount(sessionId: SessionId) + fun onCancel() + } +} diff --git a/libraries/accountselect/impl/build.gradle.kts b/libraries/accountselect/impl/build.gradle.kts new file mode 100644 index 0000000000..ea1fbd52ad --- /dev/null +++ b/libraries/accountselect/impl/build.gradle.kts @@ -0,0 +1,35 @@ +import extension.setupDependencyInjection +import extension.testCommonDependencies + +/* + * Copyright 2025 New 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.accountselect.impl" +} + +setupDependencyInjection() + +dependencies { + implementation(projects.libraries.core) + implementation(projects.libraries.androidutils) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.sessionStorage.api) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + api(projects.libraries.accountselect.api) + + testCommonDependencies(libs) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.sessionStorage.test) +} diff --git a/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectNode.kt b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectNode.kt new file mode 100644 index 0000000000..5478d9fe43 --- /dev/null +++ b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectNode.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.accountselect.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint +import io.element.android.libraries.matrix.api.core.SessionId + +@ContributesNode(AppScope::class) +@AssistedInject +class AccountSelectNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: AccountSelectPresenter, +) : Node(buildContext, plugins = plugins) { + private val callbacks = plugins.filterIsInstance() + + private fun onDismiss() { + callbacks.forEach { it.onCancel() } + } + + private fun onSelectAccount(sessionId: SessionId) { + callbacks.forEach { it.onSelectAccount(sessionId) } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + AccountSelectView( + state = state, + onDismiss = ::onDismiss, + onSelectAccount = ::onSelectAccount, + modifier = modifier, + ) + } +} diff --git a/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenter.kt b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenter.kt new file mode 100644 index 0000000000..dde07e7e38 --- /dev/null +++ b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenter.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.libraries.accountselect.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import dev.zacsweers.metro.Inject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.sessionstorage.api.SessionStore +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +@Inject +class AccountSelectPresenter( + private val sessionStore: SessionStore, +) : Presenter { + @Composable + override fun present(): AccountSelectState { + val accounts by produceState(persistentListOf()) { + // Do not use sessionStore.sessionsFlow() to not make it change when an account is selected. + value = sessionStore.getAllSessions() + .map { + MatrixUser( + userId = UserId(it.userId), + displayName = it.userDisplayName, + avatarUrl = it.userAvatarUrl, + ) + } + .toPersistentList() + } + + return AccountSelectState( + accounts = accounts, + ) + } +} diff --git a/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectState.kt b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectState.kt new file mode 100644 index 0000000000..feaedaf90d --- /dev/null +++ b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectState.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.accountselect.impl + +import io.element.android.libraries.matrix.api.user.MatrixUser +import kotlinx.collections.immutable.ImmutableList + +data class AccountSelectState( + val accounts: ImmutableList, +) diff --git a/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectStateProvider.kt b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectStateProvider.kt new file mode 100644 index 0000000000..3dc0a22b9c --- /dev/null +++ b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectStateProvider.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.libraries.accountselect.impl + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import kotlinx.collections.immutable.toPersistentList + +open class AccountSelectStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + anAccountSelectState(), + anAccountSelectState(accounts = aMatrixUserList()), + ) +} + +private fun anAccountSelectState( + accounts: List = listOf(), +) = AccountSelectState( + accounts = accounts.toPersistentList(), +) diff --git a/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectView.kt b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectView.kt new file mode 100644 index 0000000000..b589df23f6 --- /dev/null +++ b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectView.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.accountselect.impl + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.ui.components.MatrixUserRow +import io.element.android.libraries.ui.strings.CommonStrings + +@Suppress("MultipleEmitters") // False positive +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AccountSelectView( + state: AccountSelectState, + onSelectAccount: (SessionId) -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + BackHandler(onBack = { onDismiss() }) + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + titleStr = stringResource(CommonStrings.common_select_account), + navigationIcon = { + BackButton(onClick = { onDismiss() }) + }, + ) + } + ) { paddingValues -> + Column( + Modifier + .padding(paddingValues) + .consumeWindowInsets(paddingValues) + ) { + LazyColumn { + items(state.accounts, key = { it.userId }) { matrixUser -> + Column { + MatrixUserRow( + modifier = Modifier + .fillMaxWidth() + .clickable { + onSelectAccount(matrixUser.userId) + } + .padding(vertical = 8.dp), + matrixUser = matrixUser, + ) + HorizontalDivider() + } + } + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun AccountSelectViewPreview(@PreviewParameter(AccountSelectStateProvider::class) state: AccountSelectState) = ElementPreview { + AccountSelectView( + state = state, + onSelectAccount = {}, + onDismiss = {}, + ) +} diff --git a/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/DefaultAccountSelectEntryPoint.kt b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/DefaultAccountSelectEntryPoint.kt new file mode 100644 index 0000000000..baf5ecd5b3 --- /dev/null +++ b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/DefaultAccountSelectEntryPoint.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.accountselect.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint +import io.element.android.libraries.architecture.createNode + +@ContributesBinding(AppScope::class) +@Inject +class DefaultAccountSelectEntryPoint : AccountSelectEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): AccountSelectEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : AccountSelectEntryPoint.NodeBuilder { + override fun callback(callback: AccountSelectEntryPoint.Callback): AccountSelectEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } + } +} diff --git a/libraries/accountselect/impl/src/test/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenterTest.kt b/libraries/accountselect/impl/src/test/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenterTest.kt new file mode 100644 index 0000000000..27a8d7d9cf --- /dev/null +++ b/libraries/accountselect/impl/src/test/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenterTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.accountselect.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.user.MatrixUser +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.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.aSessionData +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class AccountSelectPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createAccountSelectPresenter() + presenter.test { + val initialState = awaitItem() + assertThat(initialState.accounts).isEmpty() + } + } + + @Test + fun `present - multiple accounts case`() = runTest { + val presenter = createAccountSelectPresenter( + sessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData(sessionId = A_SESSION_ID.value), + aSessionData( + sessionId = A_SESSION_ID_2.value, + userDisplayName = "Bob", + userAvatarUrl = "avatarUrl", + ), + ) + ) + ) + presenter.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.accounts).hasSize(2) + val firstAccount = initialState.accounts[0] + assertThat(firstAccount).isEqualTo( + MatrixUser( + userId = A_SESSION_ID, + displayName = null, + avatarUrl = null, + ) + ) + val secondAccount = initialState.accounts[1] + assertThat(secondAccount).isEqualTo( + MatrixUser( + userId = A_SESSION_ID_2, + displayName = "Bob", + avatarUrl = "avatarUrl", + ) + ) + } + } +} + +internal fun createAccountSelectPresenter( + sessionStore: SessionStore = InMemorySessionStore(), +) = AccountSelectPresenter( + sessionStore = sessionStore, +) diff --git a/libraries/accountselect/impl/src/test/kotlin/io/element/android/libraries/accountselect/impl/DefaultAccountSelectEntryPointTest.kt b/libraries/accountselect/impl/src/test/kotlin/io/element/android/libraries/accountselect/impl/DefaultAccountSelectEntryPointTest.kt new file mode 100644 index 0000000000..d61dcc89ba --- /dev/null +++ b/libraries/accountselect/impl/src/test/kotlin/io/element/android/libraries/accountselect/impl/DefaultAccountSelectEntryPointTest.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.libraries.accountselect.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultAccountSelectEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultAccountSelectEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + AccountSelectNode( + buildContext = buildContext, + plugins = plugins, + presenter = createAccountSelectPresenter(), + ) + } + val callback = object : AccountSelectEntryPoint.Callback { + override fun onSelectAccount(sessionId: SessionId) = lambdaError() + override fun onCancel() = lambdaError() + } + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .callback(callback) + .build() + assertThat(result).isInstanceOf(AccountSelectNode::class.java) + assertThat(result.plugins).contains(callback) + } +} diff --git a/libraries/androidutils/build.gradle.kts b/libraries/androidutils/build.gradle.kts index 1aa23a09e8..ac1e317b48 100644 --- a/libraries/androidutils/build.gradle.kts +++ b/libraries/androidutils/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -18,26 +19,22 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.di) implementation(projects.libraries.core) implementation(projects.services.toolbox.api) - implementation(libs.dagger) implementation(libs.timber) implementation(libs.androidx.corektx) implementation(libs.androidx.activity.activity) implementation(libs.androidx.recyclerview) implementation(libs.androidx.exifinterface) + implementation(libs.androidx.datastore.preferences) api(libs.androidx.browser) - testImplementation(projects.tests.testutils) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs) testImplementation(libs.coroutines.core) - testImplementation(libs.coroutines.test) testImplementation(projects.services.toolbox.test) } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/AndroidClipboardHelper.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/AndroidClipboardHelper.kt index 80f4b47d7c..98fa682060 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/AndroidClipboardHelper.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/AndroidClipboardHelper.kt @@ -11,15 +11,16 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context 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.di.SingleIn -import javax.inject.Inject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.di.annotations.ApplicationContext @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) -class AndroidClipboardHelper @Inject constructor( +@Inject +class AndroidClipboardHelper( @ApplicationContext private val context: Context, ) : ClipboardHelper { private val clipboardManager = requireNotNull(context.getSystemService()) diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/TemporaryUriDeleter.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/TemporaryUriDeleter.kt index 77c4dc3547..e8f7ef7f1d 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/TemporaryUriDeleter.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/TemporaryUriDeleter.kt @@ -9,11 +9,11 @@ package io.element.android.libraries.androidutils.file import android.content.Context import android.net.Uri -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext import timber.log.Timber -import javax.inject.Inject interface TemporaryUriDeleter { /** @@ -23,7 +23,8 @@ interface TemporaryUriDeleter { } @ContributesBinding(AppScope::class) -class DefaultTemporaryUriDeleter @Inject constructor( +@Inject +class DefaultTemporaryUriDeleter( @ApplicationContext private val context: Context, ) : TemporaryUriDeleter { private val baseCacheUri = "content://${context.packageName}.fileprovider/cache" diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatter.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatter.kt index 4c1bf4657a..9578c96b2c 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatter.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatter.kt @@ -10,14 +10,15 @@ package io.element.android.libraries.androidutils.filesize import android.content.Context import android.os.Build import android.text.format.Formatter -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider -import javax.inject.Inject @ContributesBinding(AppScope::class) -class AndroidFileSizeFormatter @Inject constructor( +@Inject +class AndroidFileSizeFormatter( @ApplicationContext private val context: Context, private val sdkIntProvider: BuildVersionSdkIntProvider, ) : FileSizeFormatter { diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelper.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelper.kt index 91953a22a3..3ac32fd3a9 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelper.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelper.kt @@ -27,7 +27,11 @@ class VideoCompressorHelper( fun getOutputSize(inputSize: Size): Size { val resultMajor = min(inputSize.major(), maxSize) val aspectRatio = inputSize.major().toFloat() / inputSize.minor().toFloat() - return Size(resultMajor, (resultMajor / aspectRatio).roundToInt()) + return if (inputSize.isLandscape()) { + Size(resultMajor, (resultMajor / aspectRatio).roundToInt()) + } else { + Size((resultMajor / aspectRatio).roundToInt(), resultMajor) + } } /** @@ -42,5 +46,6 @@ class VideoCompressorHelper( } } -internal fun Size.major(): Int = if (width > height) width else height -internal fun Size.minor(): Int = if (width < height) width else height +private fun Size.isLandscape(): Boolean = width > height +private fun Size.major(): Int = if (isLandscape()) width else height +private fun Size.minor(): Int = if (isLandscape()) height else width diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/preferences/DefaultPreferencesCorruptionHandlerFactory.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/preferences/DefaultPreferencesCorruptionHandlerFactory.kt new file mode 100644 index 0000000000..e6270f4af3 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/preferences/DefaultPreferencesCorruptionHandlerFactory.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.androidutils.preferences + +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.emptyPreferences + +object DefaultPreferencesCorruptionHandlerFactory { + /** + * Creates a [ReplaceFileCorruptionHandler] that will replace the corrupted preferences file with an empty preferences object. + */ + fun replaceWithEmpty(): ReplaceFileCorruptionHandler { + return ReplaceFileCorruptionHandler( + produceNewData = { + // If the preferences file is corrupted, we return an empty preferences object + emptyPreferences() + }, + ) + } +} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/DateTimeObserver.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/DateTimeObserver.kt index 7cae54e1ba..e3c7973341 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/DateTimeObserver.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/DateTimeObserver.kt @@ -11,15 +11,15 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.libraries.androidutils.system.DateTimeObserver.Event -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.ApplicationContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import java.time.Instant -import javax.inject.Inject interface DateTimeObserver { val changes: Flow @@ -32,7 +32,8 @@ interface DateTimeObserver { @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) -class DefaultDateTimeObserver @Inject constructor( +@Inject +class DefaultDateTimeObserver( @ApplicationContext context: Context ) : DateTimeObserver { private val dateTimeReceiver = object : BroadcastReceiver() { diff --git a/libraries/androidutils/src/main/res/values-ko/translations.xml b/libraries/androidutils/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..8e1a6fae7e --- /dev/null +++ b/libraries/androidutils/src/main/res/values-ko/translations.xml @@ -0,0 +1,4 @@ + + + "이 동작을 수행할 수 있는 앱을 찾지 못했습니다." + diff --git a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelperTest.kt b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelperTest.kt new file mode 100644 index 0000000000..96d15d6bfa --- /dev/null +++ b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelperTest.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.libraries.androidutils.media + +import android.util.Size +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class VideoCompressorHelperTest { + @Test + fun `test getOutputSize`() { + val helper = VideoCompressorHelper(maxSize = 720) + + // Landscape input + var inputSize = Size(1920, 1080) + var outputSize = helper.getOutputSize(inputSize) + assertThat(outputSize).isEqualTo(Size(720, 405)) + + // Landscape input small height + inputSize = Size(1920, 200) + outputSize = helper.getOutputSize(inputSize) + assertThat(outputSize).isEqualTo(Size(720, 75)) + + // Portrait input + inputSize = Size(1080, 1920) + outputSize = helper.getOutputSize(inputSize) + assertThat(outputSize).isEqualTo(Size(405, 720)) + + // Portrait input small width + inputSize = Size(200, 1920) + outputSize = helper.getOutputSize(inputSize) + assertThat(outputSize).isEqualTo(Size(75, 720)) + + // Square input + inputSize = Size(1000, 1000) + outputSize = helper.getOutputSize(inputSize) + assertThat(outputSize).isEqualTo(Size(720, 720)) + + // Square input same size + inputSize = Size(720, 720) + outputSize = helper.getOutputSize(inputSize) + assertThat(outputSize).isEqualTo(Size(720, 720)) + + // Square input no downscaling + inputSize = Size(240, 240) + outputSize = helper.getOutputSize(inputSize) + assertThat(outputSize).isEqualTo(Size(240, 240)) + + // Small input landscape (no downscaling) + inputSize = Size(640, 480) + outputSize = helper.getOutputSize(inputSize) + assertThat(outputSize).isEqualTo(Size(640, 480)) + + // Small input portrait (no downscaling) + inputSize = Size(480, 640) + outputSize = helper.getOutputSize(inputSize) + assertThat(outputSize).isEqualTo(Size(480, 640)) + } + + @Test + fun `test calculateOptimalBitrate`() { + val helper = VideoCompressorHelper(maxSize = 720) + val inputSize = Size(1920, 1080) + var bitrate = helper.calculateOptimalBitrate(inputSize, frameRate = 30) + // Output size will be 720x405, so bitrate = 720*405*0.1*30 = 874800 + assertThat(bitrate).isEqualTo(874_800L) + // Half frame rate, half bitrate + bitrate = helper.calculateOptimalBitrate(inputSize, frameRate = 15) + assertThat(bitrate).isEqualTo(437_400L) + } +} diff --git a/libraries/architecture/build.gradle.kts b/libraries/architecture/build.gradle.kts index c50cf39122..55b79abca0 100644 --- a/libraries/architecture/build.gradle.kts +++ b/libraries/architecture/build.gradle.kts @@ -1,3 +1,6 @@ +import extension.setupDependencyInjection +import extension.testCommonDependencies + /* * Copyright 2023, 2024 New Vector Ltd. * @@ -13,15 +16,15 @@ android { namespace = "io.element.android.libraries.architecture" } +setupDependencyInjection() + dependencies { api(projects.libraries.di) api(projects.libraries.core) - api(libs.dagger) + api(libs.metro.runtime) api(libs.appyx.core) api(libs.androidx.lifecycle.runtime) api(libs.molecule.runtime) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.truth) + testCommonDependencies(libs) } diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AssistedNodeFactory.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AssistedNodeFactory.kt index 508038fb4c..d333eae1cd 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AssistedNodeFactory.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AssistedNodeFactory.kt @@ -11,6 +11,6 @@ import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin -interface AssistedNodeFactory { +fun interface AssistedNodeFactory { fun create(buildContext: BuildContext, plugins: List): NODE } 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 f080b64d01..c02c64135d 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 @@ -10,6 +10,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 kotlinx.coroutines.TimeoutCancellationException import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -159,16 +160,19 @@ suspend inline fun runUpdatingState( callsInPlace(resultBlock, InvocationKind.EXACTLY_ONCE) } state.value = AsyncAction.Loading - return resultBlock().fold( + return try { + resultBlock() + } catch (e: TimeoutCancellationException) { + state.value = AsyncAction.Failure(errorTransform(e)) + throw e + }.fold( onSuccess = { state.value = AsyncAction.Success(it) Result.success(it) }, onFailure = { val error = errorTransform(it) - state.value = AsyncAction.Failure( - error = error, - ) + state.value = AsyncAction.Failure(error) Result.failure(error) } ) diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Bindings.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Bindings.kt index 0c922729fc..4188b0a70a 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Bindings.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Bindings.kt @@ -10,34 +10,30 @@ package io.element.android.libraries.architecture import android.content.Context import android.content.ContextWrapper import com.bumble.appyx.core.node.Node -import io.element.android.libraries.di.DaggerComponentOwner +import io.element.android.libraries.di.DependencyInjectionGraphOwner -inline fun Node.optionalBindings() = optionalBindings(T::class.java) inline fun Node.bindings() = bindings(T::class.java) inline fun Context.bindings() = bindings(T::class.java) fun Context.bindings(klass: Class): T { - // search dagger components in the context hierarchy + // search the components in the dependency injection graph return generateSequence(this) { (it as? ContextWrapper)?.baseContext } .plus(applicationContext) - .filterIsInstance() - .map { it.daggerComponent } - .flatMap { if (it is Collection<*>) it else listOf(it) } + .filterIsInstance() + .map { it.graph } + .flatMap { it as? Collection<*> ?: listOf(it) } .filterIsInstance(klass) .firstOrNull() ?: error("Unable to find bindings for ${klass.name}") } -fun Node.optionalBindings(klass: Class): T? { - // search dagger components in node hierarchy +fun Node.bindings(klass: Class): T { + // search the components in the node hierarchy return generateSequence(this, Node::parent) - .filterIsInstance() - .map { it.daggerComponent } - .flatMap { if (it is Collection<*>) it else listOf(it) } + .filterIsInstance() + .map { it.graph } + .flatMap { it as? Collection<*> ?: listOf(it) } .filterIsInstance(klass) .firstOrNull() -} - -fun Node.bindings(klass: Class): T { - return optionalBindings(klass) ?: error("Unable to find bindings for ${klass.name}") + ?: error("Unable to find bindings for ${klass.name}") } diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/FeatureEntryPoint.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/FeatureEntryPoint.kt index 2a64bb7c98..d5a932b705 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/FeatureEntryPoint.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/FeatureEntryPoint.kt @@ -18,6 +18,6 @@ interface FeatureEntryPoint /** * Can be used when the feature only exposes a simple node without the need of plugins. */ -interface SimpleFeatureEntryPoint : FeatureEntryPoint { +fun interface SimpleFeatureEntryPoint : FeatureEntryPoint { fun createNode(parentNode: Node, buildContext: BuildContext): Node } diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeFactories.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeFactories.kt index 73d2b0a064..63cb4c4ed9 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeFactories.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeFactories.kt @@ -7,10 +7,11 @@ package io.element.android.libraries.architecture -import android.content.Context import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.Multibinds +import kotlin.reflect.KClass inline fun Node.createNode( buildContext: BuildContext, @@ -20,23 +21,15 @@ inline fun Node.createNode( return bindings.createNode(buildContext, plugins) } -inline fun Context.createNode( - buildContext: BuildContext, - plugins: List = emptyList() -): N { - val bindings: NodeFactoriesBindings = bindings() - return bindings.createNode(buildContext, plugins) -} - inline fun NodeFactoriesBindings.createNode( buildContext: BuildContext, - plugins: List = emptyList() + plugins: List, ): N { - val nodeClass = N::class.java + val nodeClass = N::class val nodeFactoryMap = nodeFactories() // Note to developers: If you got the error below, make sure to build again after - // clearing the cache (sometimes several times) to let Dagger generate the NodeFactory. - val nodeFactory = nodeFactoryMap[nodeClass] ?: error("Cannot find NodeFactory for ${nodeClass.name}.") + // clearing the cache (sometimes several times) to let codegen generate the NodeFactory. + val nodeFactory = nodeFactoryMap[nodeClass] ?: error("Cannot find NodeFactory for ${nodeClass.java.name}.") @Suppress("UNCHECKED_CAST") val castedNodeFactory = nodeFactory as? AssistedNodeFactory @@ -44,6 +37,7 @@ inline fun NodeFactoriesBindings.createNode( return node as N } -interface NodeFactoriesBindings { - fun nodeFactories(): Map, AssistedNodeFactory<*>> +fun interface NodeFactoriesBindings { + @Multibinds + fun nodeFactories(): Map, AssistedNodeFactory<*>> } diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeKey.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeKey.kt index bb7dde6bd3..b5ba343b4e 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeKey.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeKey.kt @@ -8,10 +8,10 @@ package io.element.android.libraries.architecture import com.bumble.appyx.core.node.Node -import dagger.MapKey +import dev.zacsweers.metro.MapKey import kotlin.reflect.KClass @Retention(AnnotationRetention.RUNTIME) -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) @MapKey annotation class NodeKey(val value: KClass) diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/DelegateTransitionHandler.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/DelegateTransitionHandler.kt new file mode 100644 index 0000000000..642ff6fc3a --- /dev/null +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/DelegateTransitionHandler.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.architecture.appyx + +import android.annotation.SuppressLint +import androidx.compose.animation.core.Transition +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.navigation.transition.ModifierTransitionHandler +import com.bumble.appyx.core.navigation.transition.TransitionDescriptor + +/** + * A [ModifierTransitionHandler] that delegates the creation of the modifier to another handler + * based on the [NavTarget]. The idea is to allow different transitions for different [NavTarget]s. + */ +class DelegateTransitionHandler( + private val handlerProvider: (NavTarget) -> ModifierTransitionHandler, +) : ModifierTransitionHandler() { + @SuppressLint("ModifierFactoryExtensionFunction") + override fun createModifier(modifier: Modifier, transition: Transition, descriptor: TransitionDescriptor): Modifier { + return handlerProvider(descriptor.element).createModifier(modifier, transition, descriptor) + } +} + +@Composable +fun rememberDelegateTransitionHandler( + handlerProvider: (NavTarget) -> ModifierTransitionHandler, +): ModifierTransitionHandler = + remember(handlerProvider) { DelegateTransitionHandler(handlerProvider = handlerProvider) } diff --git a/libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncActionTest.kt b/libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncActionTest.kt new file mode 100644 index 0000000000..fbf892a13f --- /dev/null +++ b/libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncActionTest.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.architecture + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test +import kotlin.time.Duration.Companion.milliseconds + +class AsyncActionTest { + @Test + fun `updates state on timeout`() = runTest { + val state: MutableState> = mutableStateOf(AsyncAction.Uninitialized) + val timeoutMillis = 500L + val operationTimeMillis = 1000L + + try { + runUpdatingState(state = state) { + withTimeout(timeoutMillis.milliseconds) { + delay(operationTimeMillis) + } + Result.success(0) + } + fail("Expected TimeoutCancellationException, but nothing was thrown") + } catch (e: TimeoutCancellationException) { + assertTrue(state.value.isFailure()) + assertSame(e, state.value.errorOrNull()) + } + } +} diff --git a/libraries/audio/impl/build.gradle.kts b/libraries/audio/impl/build.gradle.kts index 61a4cd7eba..f2d9157f96 100644 --- a/libraries/audio/impl/build.gradle.kts +++ b/libraries/audio/impl/build.gradle.kts @@ -1,4 +1,4 @@ -import extension.setupAnvil +import extension.setupDependencyInjection /* * Copyright 2025 New Vector Ltd. @@ -14,12 +14,11 @@ android { namespace = "io.element.android.libraries.audio.impl" } -setupAnvil() +setupDependencyInjection() 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 index 057339d60c..f5edc2aabb 100644 --- a/libraries/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt +++ b/libraries/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt @@ -13,15 +13,16 @@ import android.media.AudioFocusRequest import android.media.AudioManager import android.os.Build import androidx.core.content.getSystemService -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 +import io.element.android.libraries.di.annotations.ApplicationContext @ContributesBinding(AppScope::class) -class DefaultAudioFocus @Inject constructor( +@Inject +class DefaultAudioFocus( @ApplicationContext private val context: Context, ) : AudioFocus { private val audioManager = requireNotNull(context.getSystemService()) diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/CoroutineDispatchers.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/CoroutineDispatchers.kt index 94bfa8e324..606fe20158 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/CoroutineDispatchers.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/CoroutineDispatchers.kt @@ -8,9 +8,18 @@ package io.element.android.libraries.core.coroutine import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers data class CoroutineDispatchers( val io: CoroutineDispatcher, val computation: CoroutineDispatcher, val main: CoroutineDispatcher, -) +) { + companion object { + val Default = CoroutineDispatchers( + io = Dispatchers.IO, + computation = Dispatchers.Default, + main = Dispatchers.Main, + ) + } +} diff --git a/libraries/cryptography/impl/build.gradle.kts b/libraries/cryptography/impl/build.gradle.kts index def2c73346..5e130b64e2 100644 --- a/libraries/cryptography/impl/build.gradle.kts +++ b/libraries/cryptography/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -15,13 +16,11 @@ android { namespace = "io.element.android.libraries.cryptography.impl" } -setupAnvil() +setupDependencyInjection() dependencies { - implementation(libs.dagger) implementation(projects.libraries.di) api(projects.libraries.cryptography.api) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) + testCommonDependencies(libs) } diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionService.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionService.kt index 19f38f29e1..7b3d57259d 100644 --- a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionService.kt +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionService.kt @@ -7,21 +7,22 @@ package io.element.android.libraries.cryptography.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.cryptography.api.AESEncryptionSpecs import io.element.android.libraries.cryptography.api.EncryptionDecryptionService import io.element.android.libraries.cryptography.api.EncryptionResult -import io.element.android.libraries.di.AppScope import javax.crypto.Cipher import javax.crypto.SecretKey import javax.crypto.spec.GCMParameterSpec -import javax.inject.Inject /** * Default implementation of [EncryptionDecryptionService] using AES encryption. */ @ContributesBinding(AppScope::class) -class AESEncryptionDecryptionService @Inject constructor() : EncryptionDecryptionService { +@Inject +class AESEncryptionDecryptionService : EncryptionDecryptionService { override fun createEncryptionCipher(key: SecretKey): Cipher { return Cipher.getInstance(AESEncryptionSpecs.CIPHER_TRANSFORMATION).apply { init(Cipher.ENCRYPT_MODE, key) diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/CryptographyModule.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/CryptographyModule.kt index ead85bf776..6990796ef3 100644 --- a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/CryptographyModule.kt +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/CryptographyModule.kt @@ -7,16 +7,16 @@ package io.element.android.libraries.cryptography.impl -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides import java.security.KeyStore internal const val ANDROID_KEYSTORE = "AndroidKeyStore" @ContributesTo(AppScope::class) -@Module +@BindingContainer object CryptographyModule { @Provides fun providesAndroidKeyStore(): KeyStore { diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt index ab593a5528..9e394f9db2 100644 --- a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt @@ -10,23 +10,24 @@ package io.element.android.libraries.cryptography.impl import android.annotation.SuppressLint import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.cryptography.api.AESEncryptionSpecs import io.element.android.libraries.cryptography.api.SecretKeyRepository -import io.element.android.libraries.di.AppScope import timber.log.Timber import java.security.KeyStore import java.security.KeyStoreException import javax.crypto.KeyGenerator import javax.crypto.SecretKey -import javax.inject.Inject /** * Default implementation of [SecretKeyRepository] that uses the Android Keystore to store the keys. * The generated key uses AES algorithm, with a key size of 128 bits, and the GCM block mode. */ @ContributesBinding(AppScope::class) -class KeyStoreSecretKeyRepository @Inject constructor( +@Inject +class KeyStoreSecretKeyRepository( private val keyStore: KeyStore, ) : SecretKeyRepository { // False positive lint issue diff --git a/libraries/dateformatter/api/build.gradle.kts b/libraries/dateformatter/api/build.gradle.kts index 88fd41f039..cebb9d4049 100644 --- a/libraries/dateformatter/api/build.gradle.kts +++ b/libraries/dateformatter/api/build.gradle.kts @@ -1,3 +1,5 @@ +import extension.testCommonDependencies + /* * Copyright 2022-2024 New Vector Ltd. * @@ -13,7 +15,6 @@ android { namespace = "io.element.android.libraries.dateformatter.api" dependencies { - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) + testCommonDependencies(libs) } } diff --git a/libraries/dateformatter/impl/build.gradle.kts b/libraries/dateformatter/impl/build.gradle.kts index ed8c7731bd..72da2f81f6 100644 --- a/libraries/dateformatter/impl/build.gradle.kts +++ b/libraries/dateformatter/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -11,7 +12,7 @@ plugins { id("io.element.android-compose-library") } -setupAnvil() +setupDependencyInjection() android { namespace = "io.element.android.libraries.dateformatter.impl" @@ -31,7 +32,6 @@ android { } dependencies { - implementation(libs.dagger) implementation(projects.libraries.core) implementation(projects.libraries.designsystem) implementation(projects.libraries.di) @@ -41,13 +41,8 @@ android { api(projects.libraries.dateformatter.api) api(libs.datetime) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs, true) testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.services.toolbox.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.androidx.compose.ui.test.junit) } } 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 bc8d8aedc0..8a9ce96463 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 @@ -7,10 +7,10 @@ package io.element.android.libraries.dateformatter.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.extensions.safeCapitalize -import io.element.android.libraries.di.AppScope -import javax.inject.Inject interface DateFormatterDay { fun format( @@ -20,7 +20,8 @@ interface DateFormatterDay { } @ContributesBinding(AppScope::class) -class DefaultDateFormatterDay @Inject constructor( +@Inject +class DefaultDateFormatterDay( private val localDateTimeProvider: LocalDateTimeProvider, private val dateFormatters: DateFormatters, ) : DateFormatterDay { diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterFull.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterFull.kt index 96b67c3227..38ff91fdc7 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterFull.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterFull.kt @@ -7,10 +7,11 @@ package io.element.android.libraries.dateformatter.impl +import dev.zacsweers.metro.Inject import io.element.android.services.toolbox.api.strings.StringProvider -import javax.inject.Inject -class DateFormatterFull @Inject constructor( +@Inject +class DateFormatterFull( private val stringProvider: StringProvider, private val localDateTimeProvider: LocalDateTimeProvider, private val dateFormatters: DateFormatters, diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterMonth.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterMonth.kt index 4e7e46986c..a6002744c4 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterMonth.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterMonth.kt @@ -7,11 +7,12 @@ package io.element.android.libraries.dateformatter.impl +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.extensions.safeCapitalize import io.element.android.services.toolbox.api.strings.StringProvider -import javax.inject.Inject -class DateFormatterMonth @Inject constructor( +@Inject +class DateFormatterMonth( private val stringProvider: StringProvider, private val localDateTimeProvider: LocalDateTimeProvider, private val dateFormatters: DateFormatters, diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTime.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTime.kt index 64dbe80415..de46c1f5c9 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTime.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTime.kt @@ -7,9 +7,10 @@ package io.element.android.libraries.dateformatter.impl -import javax.inject.Inject +import dev.zacsweers.metro.Inject -class DateFormatterTime @Inject constructor( +@Inject +class DateFormatterTime( private val localDateTimeProvider: LocalDateTimeProvider, private val dateFormatters: DateFormatters, ) { diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTimeOnly.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTimeOnly.kt index 7d9d3883ae..b9e3091a32 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTimeOnly.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTimeOnly.kt @@ -7,9 +7,10 @@ package io.element.android.libraries.dateformatter.impl -import javax.inject.Inject +import dev.zacsweers.metro.Inject -class DateFormatterTimeOnly @Inject constructor( +@Inject +class DateFormatterTimeOnly( private val localDateTimeProvider: LocalDateTimeProvider, private val dateFormatters: DateFormatters, ) { 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 9e5ffd6afe..ceac6777ba 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 @@ -8,8 +8,9 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import kotlinx.datetime.LocalDateTime import kotlinx.datetime.toInstant import kotlinx.datetime.toJavaLocalDate @@ -17,12 +18,12 @@ import kotlinx.datetime.toJavaLocalDateTime import timber.log.Timber 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( +@Inject +class DateFormatters( localeChangeObserver: LocaleChangeObserver, private val clock: Clock, private val timeZoneProvider: TimezoneProvider, diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatter.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatter.kt index fa1c058550..cd1180caa1 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatter.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatter.kt @@ -7,14 +7,15 @@ package io.element.android.libraries.dateformatter.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultDateFormatter @Inject constructor( +@Inject +class DefaultDateFormatter( private val dateFormatterFull: DateFormatterFull, private val dateFormatterMonth: DateFormatterMonth, private val dateFormatterDay: DateFormatterDay, 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 20862fd68a..a246ce2293 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,13 +7,14 @@ package io.element.android.libraries.dateformatter.impl +import dev.zacsweers.metro.Inject import kotlinx.datetime.LocalDateTime import kotlinx.datetime.toLocalDateTime -import javax.inject.Inject import kotlin.time.Clock import kotlin.time.Instant -class LocalDateTimeProvider @Inject constructor( +@Inject +class LocalDateTimeProvider( private val clock: Clock, private val timezoneProvider: TimezoneProvider, ) { diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocaleChangeObserver.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocaleChangeObserver.kt index d17507b778..773871d153 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocaleChangeObserver.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocaleChangeObserver.kt @@ -12,11 +12,11 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build -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.SingleIn -import javax.inject.Inject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.di.annotations.ApplicationContext fun interface LocaleChangeObserver { fun addListener(listener: LocaleChangeListener) @@ -28,7 +28,8 @@ interface LocaleChangeListener { @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultLocaleChangeObserver @Inject constructor( +@Inject +class DefaultLocaleChangeObserver( @ApplicationContext private val context: Context, ) : LocaleChangeObserver { init { 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 a7da8ea6ce..ac36cd3caf 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 @@ -7,16 +7,16 @@ package io.element.android.libraries.dateformatter.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides import io.element.android.libraries.dateformatter.impl.TimezoneProvider -import io.element.android.libraries.di.AppScope import kotlinx.datetime.TimeZone import java.util.Locale import kotlin.time.Clock -@Module +@BindingContainer @ContributesTo(AppScope::class) object DateFormatterModule { @Provides diff --git a/libraries/dateformatter/impl/src/main/res/values-hu/translations.xml b/libraries/dateformatter/impl/src/main/res/values-hu/translations.xml index 33778d84f1..0f4e767bf4 100644 --- a/libraries/dateformatter/impl/src/main/res/values-hu/translations.xml +++ b/libraries/dateformatter/impl/src/main/res/values-hu/translations.xml @@ -1,5 +1,5 @@ - "%1$s itt: %2$s" + "%1$s, %2$s" "Ebben a hónapban" diff --git a/libraries/dateformatter/impl/src/main/res/values-ko/translations.xml b/libraries/dateformatter/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..6712955b3f --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,5 @@ + + + "%2$s 에 %1$s" + "이번 달" + diff --git a/libraries/dateformatter/impl/src/main/res/values-ro/translations.xml b/libraries/dateformatter/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..232b0ae467 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s la %2$s" + "Luna aceasta" + diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/SingleIn.kt b/libraries/deeplink/api/build.gradle.kts similarity index 50% rename from libraries/di/src/main/kotlin/io/element/android/libraries/di/SingleIn.kt rename to libraries/deeplink/api/build.gradle.kts index a8441295a0..01d568df52 100644 --- a/libraries/di/src/main/kotlin/io/element/android/libraries/di/SingleIn.kt +++ b/libraries/deeplink/api/build.gradle.kts @@ -4,12 +4,14 @@ * 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") +} -package io.element.android.libraries.di +android { + namespace = "io.element.android.libraries.deeplink.api" +} -import javax.inject.Scope -import kotlin.reflect.KClass - -@Scope -@Retention(AnnotationRetention.RUNTIME) -annotation class SingleIn(val clazz: KClass<*>) +dependencies { + implementation(projects.libraries.matrix.api) +} diff --git a/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeepLinkCreator.kt b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeepLinkCreator.kt new file mode 100644 index 0000000000..2a29d70bd4 --- /dev/null +++ b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeepLinkCreator.kt @@ -0,0 +1,16 @@ +/* + * 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.deeplink.api + +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 + +fun interface DeepLinkCreator { + fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String +} diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkData.kt similarity index 94% rename from libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt rename to libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkData.kt index 9dc8a90509..d15652b3ee 100644 --- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt +++ b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkData.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.deeplink +package io.element.android.libraries.deeplink.api import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId diff --git a/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkParser.kt b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkParser.kt new file mode 100644 index 0000000000..d101bbc2ec --- /dev/null +++ b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkParser.kt @@ -0,0 +1,14 @@ +/* + * 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.deeplink.api + +import android.content.Intent + +fun interface DeeplinkParser { + fun getFromIntent(intent: Intent): DeeplinkData? +} diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginUserStory.kt b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/usecase/InviteFriendsUseCase.kt similarity index 55% rename from features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginUserStory.kt rename to libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/usecase/InviteFriendsUseCase.kt index 9b0cce7b98..7c8910e156 100644 --- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginUserStory.kt +++ b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/usecase/InviteFriendsUseCase.kt @@ -5,10 +5,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.login.api +package io.element.android.libraries.deeplink.api.usecase -import kotlinx.coroutines.flow.StateFlow +import android.app.Activity -interface LoginUserStory { - val loginFlowIsDone: StateFlow +fun interface InviteFriendsUseCase { + fun execute(activity: Activity) } diff --git a/libraries/deeplink/build.gradle.kts b/libraries/deeplink/impl/build.gradle.kts similarity index 70% rename from libraries/deeplink/build.gradle.kts rename to libraries/deeplink/impl/build.gradle.kts index 5e1a06bee0..074e144b3a 100644 --- a/libraries/deeplink/build.gradle.kts +++ b/libraries/deeplink/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -6,19 +7,20 @@ import extension.setupAnvil * 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.deeplink" + namespace = "io.element.android.libraries.deeplink.impl" } -setupAnvil() +setupDependencyInjection() dependencies { + api(projects.libraries.deeplink.api) implementation(projects.libraries.di) - implementation(libs.dagger) implementation(libs.androidx.corektx) implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) @@ -27,9 +29,6 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.services.toolbox.api) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.tests.testutils) } diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/Constants.kt similarity index 84% rename from libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt rename to libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/Constants.kt index b39a1c97d8..1cd98a70d3 100644 --- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt +++ b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/Constants.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.deeplink +package io.element.android.libraries.deeplink.impl internal const val SCHEME = "elementx" internal const val HOST = "open" diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeepLinkCreator.kt similarity index 60% rename from libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt rename to libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeepLinkCreator.kt index 25058e1812..7c36ecd0b1 100644 --- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt +++ b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeepLinkCreator.kt @@ -1,19 +1,24 @@ /* - * Copyright 2023, 2024 New Vector Ltd. + * Copyright 2025 New Vector Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.deeplink +package io.element.android.libraries.deeplink.impl +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.deeplink.api.DeepLinkCreator 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 javax.inject.Inject -class DeepLinkCreator @Inject constructor() { - fun room(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String { +@ContributesBinding(AppScope::class) +@Inject +class DefaultDeepLinkCreator : DeepLinkCreator { + override fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String { return buildString { append("$SCHEME://$HOST/") append(sessionId.value) diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParser.kt similarity index 71% rename from libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt rename to libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParser.kt index cdb249b7b9..e6ab87b2b3 100644 --- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt +++ b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParser.kt @@ -1,21 +1,27 @@ /* - * Copyright 2023, 2024 New Vector Ltd. + * Copyright 2025 New Vector Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.deeplink +package io.element.android.libraries.deeplink.impl import android.content.Intent import android.net.Uri +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.deeplink.api.DeeplinkData +import io.element.android.libraries.deeplink.api.DeeplinkParser 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 javax.inject.Inject -class DeeplinkParser @Inject constructor() { - fun getFromIntent(intent: Intent): DeeplinkData? { +@ContributesBinding(AppScope::class) +@Inject +class DefaultDeeplinkParser : DeeplinkParser { + override fun getFromIntent(intent: Intent): DeeplinkData? { return intent .takeIf { it.action == Intent.ACTION_VIEW } ?.data diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/usecase/DefaultInviteFriendsUseCase.kt similarity index 79% rename from libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt rename to libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/usecase/DefaultInviteFriendsUseCase.kt index dc072fc9bc..045b199237 100644 --- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt +++ b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/usecase/DefaultInviteFriendsUseCase.kt @@ -1,30 +1,35 @@ /* - * Copyright 2023, 2024 New Vector Ltd. + * Copyright 2025 New Vector Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.deeplink.usecase +package io.element.android.libraries.deeplink.impl.usecase import android.app.Activity +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.system.startSharePlainTextIntent import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase +import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.toolbox.api.strings.StringProvider import timber.log.Timber -import javax.inject.Inject import io.element.android.libraries.androidutils.R as AndroidUtilsR -class InviteFriendsUseCase @Inject constructor( +@ContributesBinding(SessionScope::class) +@Inject +class DefaultInviteFriendsUseCase( private val stringProvider: StringProvider, private val matrixClient: MatrixClient, private val buildMeta: BuildMeta, private val permalinkBuilder: PermalinkBuilder, -) { - fun execute(activity: Activity) { +) : InviteFriendsUseCase { + override fun execute(activity: Activity) { val permalinkResult = permalinkBuilder.permalinkForUser(matrixClient.sessionId) permalinkResult.fold( onSuccess = { permalink -> diff --git a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt b/libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeepLinkCreatorTest.kt similarity index 67% rename from libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt rename to libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeepLinkCreatorTest.kt index f6114fabb2..a5c943c525 100644 --- a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt +++ b/libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeepLinkCreatorTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.deeplink +package io.element.android.libraries.deeplink.impl import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -13,15 +13,15 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_THREAD_ID import org.junit.Test -class DeepLinkCreatorTest { +class DefaultDeepLinkCreatorTest { @Test - fun room() { - val sut = DeepLinkCreator() - assertThat(sut.room(A_SESSION_ID, null, null)) + fun create() { + val sut = DefaultDeepLinkCreator() + assertThat(sut.create(A_SESSION_ID, null, null)) .isEqualTo("elementx://open/@alice:server.org") - assertThat(sut.room(A_SESSION_ID, A_ROOM_ID, null)) + assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, null)) .isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain") - assertThat(sut.room(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID)) + assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID)) .isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId") } } diff --git a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt b/libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParserTest.kt similarity index 90% rename from libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt rename to libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParserTest.kt index 48e3bab9db..787c721092 100644 --- a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt +++ b/libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParserTest.kt @@ -5,11 +5,12 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.deeplink +package io.element.android.libraries.deeplink.impl import android.content.Intent import androidx.core.net.toUri import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.deeplink.api.DeeplinkData import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_THREAD_ID @@ -19,7 +20,7 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class DeeplinkParserTest { +class DefaultDeeplinkParserTest { companion object { const val A_URI = "elementx://open/@alice:server.org" @@ -29,10 +30,9 @@ class DeeplinkParserTest { "elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId" } - private val sut = DeeplinkParser() - @Test fun `nominal cases`() { + val sut = DefaultDeeplinkParser() assertThat(sut.getFromIntent(createIntent(A_URI))) .isEqualTo(DeeplinkData.Root(A_SESSION_ID)) assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM))) @@ -43,7 +43,7 @@ class DeeplinkParserTest { @Test fun `error cases`() { - val sut = DeeplinkParser() + val sut = DefaultDeeplinkParser() // Bad scheme assertThat(sut.getFromIntent(createIntent("x://open/@alice:server.org"))).isNull() // Bad host diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index 0d1efa1ed1..c2eec20b8c 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -1,3 +1,5 @@ +import extension.testCommonDependencies + /* * Copyright 2022-2024 New Vector Ltd. * @@ -42,10 +44,6 @@ android { ksp(libs.showkase.processor) implementation(libs.showkase) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoomPreviewDescriptionAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoomPreviewDescriptionAtom.kt index 8ca1a01e6b..3aece9889e 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoomPreviewDescriptionAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoomPreviewDescriptionAtom.kt @@ -15,14 +15,18 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.Text @Composable -fun RoomPreviewDescriptionAtom(description: String, modifier: Modifier = Modifier) { +fun RoomPreviewDescriptionAtom( + description: String, + modifier: Modifier = Modifier, + maxLines: Int = Int.MAX_VALUE, +) { Text( modifier = modifier, text = description, - style = ElementTheme.typography.fontBodySmRegular, + style = ElementTheme.typography.fontBodyMdRegular, textAlign = TextAlign.Center, - color = ElementTheme.colors.textSecondary, - maxLines = 3, + color = ElementTheme.colors.textPrimary, + maxLines = maxLines, overflow = TextOverflow.Ellipsis, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoomPreviewSubtitleAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoomPreviewSubtitleAtom.kt index ae705e6bfb..4fe52f5443 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoomPreviewSubtitleAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoomPreviewSubtitleAtom.kt @@ -18,7 +18,7 @@ fun RoomPreviewSubtitleAtom(subtitle: String, modifier: Modifier = Modifier) { Text( modifier = modifier, text = subtitle, - style = ElementTheme.typography.fontBodyMdRegular, + style = ElementTheme.typography.fontBodyLgRegular, textAlign = TextAlign.Center, color = ElementTheme.colors.textSecondary, ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoomPreviewTitleAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoomPreviewTitleAtom.kt index 2ede537931..2c77dc8461 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoomPreviewTitleAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoomPreviewTitleAtom.kt @@ -23,7 +23,7 @@ fun RoomPreviewTitleAtom( Text( modifier = modifier, text = title, - style = ElementTheme.typography.fontHeadingMdBold, + style = ElementTheme.typography.fontHeadingLgBold, textAlign = TextAlign.Center, fontStyle = fontStyle, color = ElementTheme.colors.textPrimary, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt index 8351226f85..c9aa5901ec 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt @@ -15,6 +15,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.contentDescription +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 @@ -28,9 +30,13 @@ fun UnreadIndicatorAtom( size: Dp = 12.dp, color: Color = ElementTheme.colors.unreadIndicator, isVisible: Boolean = true, + contentDescription: String? = null, ) { Box( modifier = modifier + .semantics { + contentDescription?.let { this.contentDescription = it } + } .size(size) .clip(CircleShape) .background(if (isVisible) color else Color.Transparent) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InviteButtonsRowMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InviteButtonsRowMolecule.kt new file mode 100644 index 0000000000..85d938d3be --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InviteButtonsRowMolecule.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.atomic.molecules + +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy +import androidx.compose.foundation.layout.Row +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.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.ButtonSize +import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun InviteButtonsRowMolecule( + onAcceptClick: () -> Unit, + onDeclineClick: () -> Unit, + modifier: Modifier = Modifier, + declineText: String = stringResource(CommonStrings.action_decline), + acceptText: String = stringResource(CommonStrings.action_accept), +) { + Row( + modifier = modifier, + horizontalArrangement = spacedBy(12.dp) + ) { + OutlinedButton( + text = declineText, + onClick = onDeclineClick, + size = ButtonSize.MediumLowPadding, + modifier = Modifier.weight(1f), + ) + Button( + text = acceptText, + onClick = onAcceptClick, + size = ButtonSize.MediumLowPadding, + modifier = Modifier.weight(1f), + ) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/RoomPreviewMembersCountMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/MembersCountMolecule.kt similarity index 86% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/RoomPreviewMembersCountMolecule.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/MembersCountMolecule.kt index 07c57d2fac..fa22b8a50a 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/RoomPreviewMembersCountMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/MembersCountMolecule.kt @@ -25,8 +25,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text @Composable -fun RoomPreviewMembersCountMolecule( - memberCount: Long, +fun MembersCountMolecule( + memberCount: Int, modifier: Modifier = Modifier, ) { Row( @@ -51,13 +51,13 @@ fun RoomPreviewMembersCountMolecule( @PreviewsDayNight @Composable -internal fun RoomPreviewMembersCountMoleculePreview() = ElementPreview { +internal fun MembersCountMoleculePreview() = ElementPreview { Column( modifier = Modifier.padding(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { - RoomPreviewMembersCountMolecule(memberCount = 1) - RoomPreviewMembersCountMolecule(memberCount = 888) - RoomPreviewMembersCountMolecule(memberCount = 123_456) + MembersCountMolecule(memberCount = 1) + MembersCountMolecule(memberCount = 888) + MembersCountMolecule(memberCount = 123_456) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/RoomPreviewOrganism.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/RoomPreviewOrganism.kt index 511eb298ea..dac9f00a7c 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/RoomPreviewOrganism.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/RoomPreviewOrganism.kt @@ -34,14 +34,13 @@ fun RoomPreviewOrganism( title() Spacer(modifier = Modifier.height(8.dp)) subtitle() - Spacer(modifier = Modifier.height(8.dp)) if (memberCount != null) { + Spacer(modifier = Modifier.height(8.dp)) memberCount() } - Spacer(modifier = Modifier.height(8.dp)) if (description != null) { + Spacer(modifier = Modifier.height(16.dp)) description() } - Spacer(modifier = Modifier.height(24.dp)) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt index 716d4a46ce..810ff7b493 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.designsystem.theme.components.Text * * Ref: https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=133-5427&t=5SHVppfYzjvkEywR-0 * @param modifier Classical modifier. + * @param renderBackground whether to render the background image or not. * @param contentAlignment horizontal alignment of the contents. * @param footer optional footer. * @param content main content. @@ -38,6 +39,7 @@ import io.element.android.libraries.designsystem.theme.components.Text @Composable fun OnBoardingPage( modifier: Modifier = Modifier, + renderBackground: Boolean = true, contentAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, footer: @Composable () -> Unit = {}, content: @Composable () -> Unit = {}, @@ -47,13 +49,15 @@ fun OnBoardingPage( .fillMaxSize() ) { // BG - Image( - modifier = Modifier - .fillMaxSize(), - painter = painterResource(id = R.drawable.onboarding_bg), - contentScale = ContentScale.Crop, - contentDescription = null, - ) + if (renderBackground) { + Image( + modifier = Modifier + .fillMaxSize(), + painter = painterResource(id = R.drawable.onboarding_bg), + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } Column( modifier = Modifier .fillMaxSize() diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ExpandableBottomSheetLayout.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ExpandableBottomSheetLayout.kt index 66b404f554..1caf5c50b7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ExpandableBottomSheetLayout.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ExpandableBottomSheetLayout.kt @@ -60,7 +60,7 @@ import kotlin.math.roundToInt @Composable fun ExpandableBottomSheetLayout( - sheetDragHandle: @Composable BoxScope.() -> Unit, + sheetDragHandle: @Composable BoxScope.(toggleAction: () -> Unit) -> Unit, bottomSheetContent: @Composable ColumnScope.() -> Unit, state: ExpandableBottomSheetLayoutState, maxBottomSheetContentHeight: Dp, @@ -152,7 +152,19 @@ fun ExpandableBottomSheetLayout( } ) { Box(Modifier.fillMaxWidth()) { - sheetDragHandle() + sheetDragHandle { + coroutineScope.launch { + val destination = if (state.position == ExpandableBottomSheetLayoutState.Position.EXPANDED) { + state.internalPosition = ExpandableBottomSheetLayoutState.Position.COLLAPSED + minBottomContentHeightPx.toFloat() + } else { + state.internalPosition = ExpandableBottomSheetLayoutState.Position.EXPANDED + calculatedMaxBottomContentHeightPx.toFloat() + } + animatable.snapTo(currentBottomContentHeightPx.toFloat()) + animatable.animateTo(destination) + } + } } bottomSheetContent() } 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 index eda4cd73b2..71fb2d06be 100644 --- 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 @@ -38,7 +38,7 @@ class ExpandableBottomSheetLayoutState { /** * The current position of the bottom sheet layout. */ - val position = internalPosition + val position get() = internalPosition /** * The percentage of the bottom sheet layout that is currently being dragged. 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 f5d8a50ce0..fe18daaaea 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 @@ -38,6 +38,18 @@ import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.ui.strings.CommonStrings import timber.log.Timber +/** + * A progress dialog, with a spinner, and optional text content. + * + * @param modifier + * @param text Optional text to show under the spinner. + * @param type + * @param properties + * @param showCancelButton + * @param onDismissRequest + * @param content Optional additional content to show under the spinner, and above the cancel button (if shown). If both `text` and `content` are supplied, + * `text` is shown above `content`. + */ @Composable fun ProgressDialog( modifier: Modifier = Modifier, @@ -46,6 +58,7 @@ fun ProgressDialog( properties: DialogProperties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), showCancelButton: Boolean = false, onDismissRequest: () -> Unit = {}, + content: @Composable () -> Unit = {}, ) { DisposableEffect(Unit) { onDispose { @@ -75,7 +88,8 @@ fun ProgressDialog( ) } } - } + }, + content, ) } } @@ -96,7 +110,8 @@ private fun ProgressDialogContent( CircularProgressIndicator( color = ElementTheme.colors.iconPrimary ) - } + }, + content: @Composable () -> Unit, ) { Box( contentAlignment = Alignment.Center, @@ -118,6 +133,7 @@ private fun ProgressDialogContent( color = ElementTheme.colors.textPrimary, ) } + content() if (showCancelButton) { Spacer(modifier = Modifier.height(24.dp)) Box( @@ -138,7 +154,7 @@ private fun ProgressDialogContent( @Composable internal fun ProgressDialogContentPreview() = ElementThemedPreview { DialogPreview { - ProgressDialogContent(text = "test dialog content", showCancelButton = true) + ProgressDialogContent(text = "test dialog content", showCancelButton = true, content = {}) } } @@ -147,3 +163,34 @@ internal fun ProgressDialogContentPreview() = ElementThemedPreview { internal fun ProgressDialogPreview() = ElementPreview { ProgressDialog(text = "test dialog content", showCancelButton = true) } + +@PreviewsDayNight +@Composable +internal fun ProgressDialogWithContentPreview() = ElementPreview { + ProgressDialog(showCancelButton = true) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Heading", + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontHeadingSmMedium, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Subtext", + color = ElementTheme.colors.textSecondary, + style = MaterialTheme.typography.bodyMedium, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun ProgressDialogWithTextAndContentPreview() = ElementPreview { + ProgressDialog(text = "Text Content") { + Text( + text = "blah blah", + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontHeadingSmMedium, + ) + } +} 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 3c209df2cf..7ccebd2082 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 @@ -16,8 +16,8 @@ open class AvatarDataProvider : PreviewParameterProvider { .map { sequenceOf( anAvatarData(size = it), - anAvatarData(size = it).copy(name = null), - anAvatarData(size = it).copy(url = "aUrl"), + anAvatarData(size = it, name = null), + anAvatarData(size = it, url = "aUrl"), ) } .flatten() @@ -26,10 +26,12 @@ open class AvatarDataProvider : PreviewParameterProvider { fun anAvatarData( // Let's the id not start with a 'a'. id: String = "@id_of_alice:server.org", - name: String = "Alice", + name: String? = "Alice", + url: String? = null, size: AvatarSize = AvatarSize.RoomListItem, ) = AvatarData( id = id, name = name, + url = url, size = size, ) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/AvatarRow.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarRow.kt similarity index 70% rename from features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/AvatarRow.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarRow.kt index 992098dc91..8c3ae3aac9 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/AvatarRow.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarRow.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.knockrequests.impl.banner +package io.element.android.libraries.designsystem.components.avatar import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding @@ -23,10 +23,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.LayoutDirection 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.components.avatar.internal.OverlapRatioProvider import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toPx @@ -41,6 +38,7 @@ import kotlinx.collections.immutable.toImmutableList * @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 + * @param lastOnTop if true, the last visible avatar will be rendered on top. */ @Composable fun AvatarRow( @@ -48,6 +46,7 @@ fun AvatarRow( avatarType: AvatarType, modifier: Modifier = Modifier, overlapRatio: Float = 0.5f, + lastOnTop: Boolean = false, ) { val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl Box( @@ -57,23 +56,35 @@ fun AvatarRow( val avatarSize = avatarDataList.firstOrNull()?.size?.dp ?: return val avatarSizePx = avatarSize.toPx() avatarDataList - .reversed() + .let { + if (lastOnTop) { + it + } else { + it.reversed() + } + } .forEachIndexed { index, avatarData -> + val startPadding = if (lastOnTop) { + avatarSize * (1 - overlapRatio) * index + } else { + avatarSize * (1 - overlapRatio) * (lastItemIndex - index) + } Avatar( modifier = Modifier - .padding(start = avatarSize * (1 - overlapRatio) * (lastItemIndex - index)) + .padding(start = startPadding) .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } .drawWithContent { - // Draw content and clear the pixels for the avatar on the left (right in RTL). + // Draw content and clear the pixels for the avatar on the left (right in RTL) or when lastOnTop is true on + // the right (left in RTL). drawContent() - val xOffset = if (isRtl) { - size.width - avatarSizePx * (overlapRatio - 0.5f) - } else { - 0f + avatarSizePx * (overlapRatio - 0.5f) - } if (index < lastItemIndex) { + val xOffset = if (isRtl == lastOnTop) { + avatarSizePx * (overlapRatio - 0.5f) + } else { + size.width - avatarSizePx * (overlapRatio - 0.5f) + } drawCircle( color = Color.Black, center = Offset( @@ -104,6 +115,17 @@ internal fun AvatarRowPreview(@PreviewParameter(OverlapRatioProvider::class) ove } } +@Composable +@PreviewsDayNight +internal fun AvatarRowLastOnTopPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) { + ElementPreview { + ContentToPreview( + overlapRatio = overlapRatio, + lastOnTop = true, + ) + } +} + @Composable @PreviewsDayNight internal fun AvatarRowRtlPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) { @@ -117,7 +139,25 @@ internal fun AvatarRowRtlPreview(@PreviewParameter(OverlapRatioProvider::class) } @Composable -private fun ContentToPreview(overlapRatio: Float) { +@PreviewsDayNight +internal fun AvatarRowLastOnTopRtlPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) { + CompositionLocalProvider( + LocalLayoutDirection provides LayoutDirection.Rtl, + ) { + ElementPreview { + ContentToPreview( + overlapRatio = overlapRatio, + lastOnTop = true, + ) + } + } +} + +@Composable +private fun ContentToPreview( + overlapRatio: Float, + lastOnTop: Boolean = false, +) { AvatarRow( avatarDataList = listOf("A", "B", "C").map { AvatarData( @@ -128,5 +168,6 @@ private fun ContentToPreview(overlapRatio: Float) { }.toImmutableList(), avatarType = AvatarType.User, overlapRatio = overlapRatio, + lastOnTop = lastOnTop, ) } 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 622fd54547..31c569c870 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 @@ -14,9 +14,11 @@ enum class AvatarSize(val dp: Dp) { CurrentUserTopBar(32.dp), IncomingCall(140.dp), - RoomHeader(96.dp), + RoomDetailsHeader(96.dp), RoomListItem(52.dp), + SpaceListItem(52.dp), + RoomSelectRoomListItem(36.dp), UserPreference(56.dp), @@ -32,6 +34,7 @@ enum class AvatarSize(val dp: Dp) { TimelineRoom(32.dp), TimelineSender(32.dp), TimelineReadReceipt(16.dp), + TimelineThreadLatestEventSender(24.dp), ComposerAlert(32.dp), @@ -63,4 +66,13 @@ enum class AvatarSize(val dp: Dp) { DmCreationConfirmation(64.dp), UserVerification(52.dp), + + OrganizationHeader(64.dp), + SpaceHeader(64.dp), + RoomPreviewHeader(64.dp), + RoomPreviewInviter(56.dp), + SpaceMember(24.dp), + LeaveSpaceRoom(32.dp), + + AccountItem(32.dp), } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/OverlapRatioProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/OverlapRatioProvider.kt similarity index 85% rename from features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/OverlapRatioProvider.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/OverlapRatioProvider.kt index f9ff9ee82e..9a1abcee58 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/OverlapRatioProvider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/OverlapRatioProvider.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.knockrequests.impl.banner +package io.element.android.libraries.designsystem.components.avatar.internal import androidx.compose.ui.tooling.preview.PreviewParameterProvider 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 51fe275ee1..7811c26c4a 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 @@ -142,7 +142,7 @@ internal fun SimpleAlertDialogContent( Text( text = titleText, style = ElementTheme.typography.fontHeadingSmMedium, - textAlign = TextAlign.Center, + textAlign = if (icon != null) TextAlign.Center else TextAlign.Start, ) } }, @@ -510,3 +510,43 @@ internal fun DialogWithThirdButtonPreview() { } } } + +@Preview(group = PreviewGroup.Dialogs, name = "Dialog with a very long title") +@Composable +@Suppress("MaxLineLength") +internal fun DialogWithVeryLongTitlePreview() { + ElementThemedPreview(showBackground = false) { + DialogPreview { + SimpleAlertDialogContent( + title = "Dialog Title that takes more than one line", + content = "A dialog is a type of modal window that appears in front of app content to provide critical information," + + " or prompt for a decision to be made. Learn more", + submitText = "OK", + onSubmitClick = {}, + ) + } + } +} + +@Preview(group = PreviewGroup.Dialogs, name = "Dialog with a very long title and icon") +@Composable +@Suppress("MaxLineLength") +internal fun DialogWithVeryLongTitleAndIconPreview() { + ElementThemedPreview(showBackground = false) { + DialogPreview { + SimpleAlertDialogContent( + icon = { + Icon( + imageVector = CompoundIcons.NotificationsSolid(), + contentDescription = null + ) + }, + title = "Dialog Title that takes more than one line", + content = "A dialog is a type of modal window that appears in front of app content to provide critical information," + + " or prompt for a decision to be made. Learn more", + submitText = "OK", + onSubmitClick = {}, + ) + } + } +} 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 e75b7727eb..bfceb2263e 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 @@ -23,7 +23,10 @@ import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape @@ -67,16 +70,19 @@ fun SearchBar( ) { val focusManager = LocalFocusManager.current - if (!active) { - onQueryChange("") - focusManager.clearFocus() + val updatedOnQueryChange by rememberUpdatedState(onQueryChange) + LaunchedEffect(active) { + if (!active) { + updatedOnQueryChange("") + focusManager.clearFocus() + } } SearchBar( inputField = { SearchBarDefaults.InputField( query = query, - onQueryChange = onQueryChange, + onQueryChange = updatedOnQueryChange, onSearch = { focusManager.clearFocus() }, expanded = active, onExpandedChange = onActiveChange, diff --git a/libraries/di/build.gradle.kts b/libraries/di/build.gradle.kts index cd535b1e39..1b2b04d880 100644 --- a/libraries/di/build.gradle.kts +++ b/libraries/di/build.gradle.kts @@ -11,5 +11,5 @@ plugins { } dependencies { - api(libs.inject) + api(libs.metro.runtime) } diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/BaseDirectory.kt b/libraries/di/src/main/kotlin/io/element/android/libraries/di/BaseDirectory.kt new file mode 100644 index 0000000000..bc07597456 --- /dev/null +++ b/libraries/di/src/main/kotlin/io/element/android/libraries/di/BaseDirectory.kt @@ -0,0 +1,27 @@ +/* + * 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.libraries.di + +import dev.zacsweers.metro.Qualifier + +/** + * Qualifies a [File] object which represents the application base directory. + */ +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@Qualifier +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FIELD, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.VALUE_PARAMETER, + AnnotationTarget.TYPE, +) +public annotation class BaseDirectory diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/CacheDirectory.kt b/libraries/di/src/main/kotlin/io/element/android/libraries/di/CacheDirectory.kt index cf319d633b..e17f38562a 100644 --- a/libraries/di/src/main/kotlin/io/element/android/libraries/di/CacheDirectory.kt +++ b/libraries/di/src/main/kotlin/io/element/android/libraries/di/CacheDirectory.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.di -import javax.inject.Qualifier +import dev.zacsweers.metro.Qualifier /** * Qualifies a [File] object which represents the application cache directory. @@ -15,4 +15,13 @@ import javax.inject.Qualifier @Retention(AnnotationRetention.RUNTIME) @MustBeDocumented @Qualifier -annotation class CacheDirectory +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FIELD, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.VALUE_PARAMETER, + AnnotationTarget.TYPE, +) +public annotation class CacheDirectory diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/DaggerComponentOwner.kt b/libraries/di/src/main/kotlin/io/element/android/libraries/di/DependencyInjectionGraphOwner.kt similarity index 55% rename from libraries/di/src/main/kotlin/io/element/android/libraries/di/DaggerComponentOwner.kt rename to libraries/di/src/main/kotlin/io/element/android/libraries/di/DependencyInjectionGraphOwner.kt index 0d7a398a6e..f1466d924f 100644 --- a/libraries/di/src/main/kotlin/io/element/android/libraries/di/DaggerComponentOwner.kt +++ b/libraries/di/src/main/kotlin/io/element/android/libraries/di/DependencyInjectionGraphOwner.kt @@ -8,10 +8,10 @@ package io.element.android.libraries.di /** - * A [DaggerComponentOwner] is anything that "owns" a Dagger Component. + * A [DependencyInjectionGraphOwner] is anything that "owns" a DI Graph. * */ -interface DaggerComponentOwner { - /** This is either a component, or a list of components. */ - val daggerComponent: Any +interface DependencyInjectionGraphOwner { + /** This is either a graph, or a list of graphs. */ + val graph: Any } 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 index ea597e56e1..9ebf83583e 100644 --- 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 @@ -7,7 +7,7 @@ package io.element.android.libraries.di.annotations -import javax.inject.Qualifier +import dev.zacsweers.metro.Qualifier /** * Qualifies a [CoroutineScope] object which represents the base coroutine scope to use for the application. diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/ApplicationContext.kt b/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/ApplicationContext.kt similarity index 73% rename from libraries/di/src/main/kotlin/io/element/android/libraries/di/ApplicationContext.kt rename to libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/ApplicationContext.kt index 27800192f7..ad70c53bce 100644 --- a/libraries/di/src/main/kotlin/io/element/android/libraries/di/ApplicationContext.kt +++ b/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/ApplicationContext.kt @@ -1,13 +1,13 @@ /* - * Copyright 2022-2024 New Vector Ltd. + * Copyright 2025 New Vector Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.di +package io.element.android.libraries.di.annotations -import javax.inject.Qualifier +import dev.zacsweers.metro.Qualifier /** * Qualifies a [Context] object that represents the application context. diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/SessionCoroutineScope.kt b/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/SessionCoroutineScope.kt index 2d635ceee2..473eaaf90c 100644 --- a/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/SessionCoroutineScope.kt +++ b/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/SessionCoroutineScope.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.di.annotations -import javax.inject.Qualifier +import dev.zacsweers.metro.Qualifier /** * Qualifies a [CoroutineScope] object which represents the base coroutine scope to use for an active session. diff --git a/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/TimelineEventFormatter.kt b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/TimelineEventFormatter.kt index c22ae6eb13..71920f3c4e 100644 --- a/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/TimelineEventFormatter.kt +++ b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/TimelineEventFormatter.kt @@ -7,8 +7,19 @@ package io.element.android.libraries.eventformatter.api +import io.element.android.libraries.matrix.api.core.UserId +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.getDisambiguatedDisplayName interface TimelineEventFormatter { - fun format(event: EventTimelineItem): CharSequence? + fun format(event: EventTimelineItem): CharSequence? { + return format( + content = event.content, + isOutgoing = event.isOwn, + sender = event.sender, + senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender), + ) + } + fun format(content: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): CharSequence? } diff --git a/libraries/eventformatter/impl/build.gradle.kts b/libraries/eventformatter/impl/build.gradle.kts index 372325286f..3b726da654 100644 --- a/libraries/eventformatter/impl/build.gradle.kts +++ b/libraries/eventformatter/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.core) @@ -32,9 +33,7 @@ dependencies { implementation(projects.services.toolbox.api) api(projects.libraries.eventformatter.api) + testCommonDependencies(libs) testImplementation(projects.services.toolbox.impl) - testImplementation(libs.test.junit) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.truth) testImplementation(projects.libraries.matrix.test) } diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt index 7c883f17e9..c172377ff5 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt @@ -9,7 +9,8 @@ package io.element.android.libraries.eventformatter.impl import androidx.annotation.StringRes import androidx.compose.ui.text.AnnotatedString -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.di.SessionScope import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter import io.element.android.libraries.matrix.api.permalink.PermalinkParser @@ -35,10 +36,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getDisambigua import io.element.android.libraries.matrix.ui.messages.toPlainText import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.toolbox.api.strings.StringProvider -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultPinnedMessagesBannerFormatter @Inject constructor( +@Inject +class DefaultPinnedMessagesBannerFormatter( private val sp: StringProvider, private val permalinkParser: PermalinkParser, ) : PinnedMessagesBannerFormatter { diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt index d6aabb5f91..8354052a24 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt @@ -7,7 +7,8 @@ package io.element.android.libraries.eventformatter.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.di.SessionScope import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.impl.mode.RenderingMode @@ -42,10 +43,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getDisambigua import io.element.android.libraries.matrix.ui.messages.toPlainText import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.toolbox.api.strings.StringProvider -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultRoomLastMessageFormatter @Inject constructor( +@Inject +class DefaultRoomLastMessageFormatter( private val sp: StringProvider, private val roomMembershipContentFormatter: RoomMembershipContentFormatter, private val profileChangeContentFormatter: ProfileChangeContentFormatter, diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt index a5f806d7b3..6cbe734733 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt @@ -7,12 +7,15 @@ package io.element.android.libraries.eventformatter.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.SessionScope import io.element.android.libraries.eventformatter.api.TimelineEventFormatter import io.element.android.libraries.eventformatter.impl.mode.RenderingMode +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent +import io.element.android.libraries.matrix.api.timeline.item.event.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 @@ -29,10 +32,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnknownConten import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.toolbox.api.strings.StringProvider -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultTimelineEventFormatter @Inject constructor( +@Inject +class DefaultTimelineEventFormatter( private val sp: StringProvider, private val buildMeta: BuildMeta, private val roomMembershipContentFormatter: RoomMembershipContentFormatter, @@ -42,12 +45,16 @@ class DefaultTimelineEventFormatter @Inject constructor( override fun format(event: EventTimelineItem): CharSequence? { val isOutgoing = event.isOwn val senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender) - return when (val content = event.content) { + return format(event.content, isOutgoing, event.sender, senderDisambiguatedDisplayName) + } + + override fun format(content: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): CharSequence? { + return when (content) { is RoomMembershipContent -> { roomMembershipContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing) } is ProfileChangeContent -> { - profileChangeContentFormatter.format(content, event.sender, senderDisambiguatedDisplayName, isOutgoing) + profileChangeContentFormatter.format(content, sender, senderDisambiguatedDisplayName, isOutgoing) } is StateContent -> { stateContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing, RenderingMode.Timeline) @@ -65,7 +72,7 @@ class DefaultTimelineEventFormatter @Inject constructor( is FailedToParseStateContent, is UnknownContent -> { if (buildMeta.isDebuggable) { - error("You should not use this formatter for this event: $event") + error("You should not use this formatter for this event content: $content") } sp.getString(CommonStrings.common_unsupported_event) } diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/ProfileChangeContentFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/ProfileChangeContentFormatter.kt index 91ae9b014b..4bf73325af 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/ProfileChangeContentFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/ProfileChangeContentFormatter.kt @@ -7,12 +7,13 @@ package io.element.android.libraries.eventformatter.impl +import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent import io.element.android.services.toolbox.api.strings.StringProvider -import javax.inject.Inject -class ProfileChangeContentFormatter @Inject constructor( +@Inject +class ProfileChangeContentFormatter( private val sp: StringProvider, ) { fun format( 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 5d6e695b7c..7fb68ac167 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 @@ -7,14 +7,15 @@ package io.element.android.libraries.eventformatter.impl +import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.services.toolbox.api.strings.StringProvider import timber.log.Timber -import javax.inject.Inject -class RoomMembershipContentFormatter @Inject constructor( +@Inject +class RoomMembershipContentFormatter( private val matrixClient: MatrixClient, private val sp: StringProvider, ) { diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt index 3f0f4a1dc2..6c6302f38b 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt @@ -7,15 +7,16 @@ package io.element.android.libraries.eventformatter.impl +import dev.zacsweers.metro.Inject import io.element.android.libraries.eventformatter.impl.mode.RenderingMode import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.toolbox.api.strings.StringProvider import timber.log.Timber -import javax.inject.Inject -class StateContentFormatter @Inject constructor( +@Inject +class StateContentFormatter( private val sp: StringProvider, ) { fun format( 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 b1f3fe1f11..9bfcdfe6a6 100644 --- a/libraries/eventformatter/impl/src/main/res/values-bg/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-bg/translations.xml @@ -26,7 +26,11 @@ "%1$s получи достъп до %2$s" "Вие позволихте на %1$s да се присъедини" "Вие поискахте да се присъедините" + "%1$s отхвърли заявката на %2$s за присъединяване" + "Вие отхвърлихте заявката на %1$s за присъединяване" + "%1$s отхвърли вашата заявка за присъединяване" "%1$s вече не се интересува от присъединяване" + "Вие отменихте заявката си за присъединяване" "%1$s напусна стаята" "Вие напуснахте стаята" "%1$s промени името на стаята на: %2$s" @@ -39,14 +43,19 @@ "Вие променихте закачените съобщения" "%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 направи неизвестна промяна в членството си" 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 7069bf6b3f..75e123cae9 100644 --- a/libraries/eventformatter/impl/src/main/res/values-de/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml @@ -2,72 +2,72 @@ "(Avatar wurde auch geändert)" "%1$s hat den Avatar geändert" - "Sie änderten ihren Avatar" + "Du hast deinen Avatar geändert" "%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)" - "Sie haben Ihren Anzeigenamen entfernt (er lautete %1$s)" + "Du hast deinen Anzeigenamen entfernt (war %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 Admin" "%1$s ist jetzt Moderator*in" - "%1$s hat den Raum-Avatar geändert" - "Dir haben den Zimmer-Avatar geändert" - "%1$s hat den Raum-Avatar entfernt" - "Sie haben den Raum-Avatar entfernt" + "%1$s hat den Chat -Avatar geändert" + "Du hast den Chat-Avatar geändert" + "%1$s hat den Chat-Avatar entfernt" + "Du hast den Chat-Avatar entfernt" "%1$s hat %2$s gesperrt" - "Sie haben %1$s gesperrt" - "Sie haben %1$s gesperrt: %2$s" + "Du hast %1$s gesperrt" + "Du hast %1$s gesperrt: %2$s" "%1$s sperrte %2$s: %3$s" - "%1$s hat den Raum erstellt" - "Sie erstellten den Chatroom" + "%1$s hat den Chat erstellt" + "Du hast den Chat erstellt" "%1$s hat %2$s eingeladen" "%1$s hat die Einladung angenommen" - "Sie haben die Einladung angenommen" - "Sie luden %1$s ein" + "Du hast die Einladung angenommen" + "Du hast %1$s eingeladen" "%1$s hat dich eingeladen" - "%1$s hat den Raum betreten" - "Sie sind dem Raum beigetreten" - "%1$s beantragt den Beitritt" + "%1$s ist dem Chat beigetreten" + "Du bist dem Chat beigetreten" + "%1$s fragt den Beitritt an" "%1$s hat %2$s den Beitritt erlaubt" - "Sie genehmigten die Beitrittsanfrage für %1$s" - "Sie haben um Beitritt gebeten" + "Du hast %1$s den Beitritt erlaubt" + "Du hast angefragt beizutreten" "%1$s hat die Beitrittsanfrage von %2$s abgelehnt" - "Sie haben die Beitrittsanfrage für %1$s abgelehnt" + "Du hast die Beitrittsanfrage von %1$s abgelehnt" "%1$s hat deine Beitrittsanfrage abgelehnt" "%1$s ist nicht mehr an einem Beitritt interessiert" - "Sie haben ihre Beitrittsanfrage widerrufen" - "%1$s hat den Raum verlassen" - "Sie verließen den Raum" - "%1$s hat den Raum-Namen geändert in: %2$s" - "Sie haben den Raumnamen geändert in: %1$s" - "%1$s hat den Raum-Namen entfernt" - "Sie haben den Raumnamen entfernt" + "Du hast deine Beitrittsanfrage abgebrochen" + "%1$s hat den Chat verlassen" + "Du hast den Chat verlassen" + "%1$s hat den Chat-Namen geändert in: %2$s" + "Du hast den Chat-Namen geändert in: %1$s" + "%1$s hat den Chat-Namen entfernt" + "Du hast den Chat-Namen entfernt" "%1$s hat keine Änderungen vorgenommen" - "Sie haben keine Änderungen vorgenommen" + "Du hast keine Änderungen vorgenommen" "%1$s hat die fixierten Nachrichten geändert" - "Sie haben die angehefteten Nachrichten geändert." + "Du hast die fixierten Nachrichten geändert" "%1$s fixierte eine Nachricht" - "Sie haben eine Nachricht angeheftet." + "Du hast eine Nachricht fixiert" "%1$s löste eine Nachricht" - "Sie haben eine angeheftete Nachricht entfernt." + "Du hast eine Nachricht gelöst" "%1$s lehnte die Einladung ab" - "Sie lehnten die Einladung ab" + "Du hast die Einladung abgelehnt" "%1$s hat %2$s entfernt" - "Sie haben %1$s entfernt" - "Sie haben %1$s entfernt: %2$s" + "Du hast %1$s entfernt" + "Du hast %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" - "Sie haben die Einladung für %1$s widerrufen" + "%1$s hat %2$s eingeladen, den Chat zu beizutreten" + "Du hast eine Einladung an %1$s gesendet, dem Chat beizutreten" + "%1$s hat die Einladung an %2$s zum Beitritt des Chat zurückgezogen" + "Du hast die Einladung an %1$s zum Beitritt des Chat zurückgezogen" "%1$s hat das Thema geändert in: %2$s" - "Sie haben das Thema geändert in:%1$s" - "%1$s hat das Raum-Thema entfernt" - "Sie haben das Raumthema entfernt" + "Du hast das Thema geändert in: %1$s" + "%1$s hat das Chat-Thema entfernt" + "Du hast das Chat-Thema entfernt" "%1$s hat die Sperre für %2$s aufgehoben" - "Sie haben die Sperre für %1$s aufgehoben" + "Du hast die Sperre für %1$s aufgehoben" "%1$s hat eine unbekannte Änderung vorgenommen" diff --git a/libraries/eventformatter/impl/src/main/res/values-ko/translations.xml b/libraries/eventformatter/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..7217449be5 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,73 @@ + + + "(프로필 사진도 변경됨)" + "%1$s님이 프로필 사진을 변경함" + "프로필 사진을 변경함" + "%1$s 회원으로 강등되었습니다" + "%1$s 중재자로 강등되었습니다" + "%1$s님이 표시 이름을 %2$s에서 %3$s(으)로 변경했습니다." + "표시 이름을 %1$s에서 %2$s(으)로 변경했습니다" + "%1$s님이 표시 이름을 제거했습니다 (이전 이름 %2$s)" + "표시 이름을 제거했습니다 (이전 이름 %1$s)" + "%1$s님이 표시되는 이름을 %2$s(으)로 변경함" + "%1$s(으)로 표시되는 이름을 변경함" + "%1$s 는 관리자로 승진되었습니다" + "%1$s 는 중재자로 승진되었습니다" + "%1$s님이 방 아바타를 변경함" + "방 아바타를 변경함" + "%1$s님이 방 아바타를 삭제함" + "방 아바타를 삭제함" + "%1$s님이 %2$s님을 차단함" + "%1$s님을 차단함" + "당신은 차단했습니다 %1$s: %2$s" + "%1$s 차단됨 %2$s: %3$s" + "%1$s님이 방을 생성함" + "방을 생성함" + "%1$s님이 %2$s님을 초대함" + "%1$s님이 초대를 수락함" + "초대를 수락함" + "%1$s님을 초대함" + "%1$s님으로부터 초대받음" + "%1$s님이 방에 참석함" + "방에 참석함" + "%1$s님이 참가를 요청함" + "%1$s님이 %2$s님의 참가를 승인함" + "%1$s님이 참가를 승인함" + "참가를 요청함" + "%1$s이 %2$s의 참가 요청을 거절함" + "%1$s님의 가입 요청을 거부했습니다." + "%1$s님의 가입 요청을 거부했습니다" + "%1$s이 참가 요청에 관심이 없음" + "참가 요청을 거부함" + "%1$s님이 방을 떠남" + "방을 떠남" + "%1$s님이 방 이름을 변경함: %2$s" + "방 이름을 변경함: %1$s" + "%1$s님이 방 이름을 삭제함" + "방 이름을 삭제함" + "%1$s 변경 사항 없음" + "변경 사항이 없습니다." + "%1$s 고정된 메시지가 변경되었습니다." + "고정된 메시지가 변경되었습니다" + "%1$s 메시지 고정" + "당신은 메시지를 고정했습니다." + "%1$s 메시지 고정 해제" + "당신은 메시지 고정 해제" + "%1$s님이 초대를 거부함" + "초대를 거부함" + "%1$s님이 %2$s님을 제거함" + "%1$s님을 제거함" + "제거했습니다 %1$s :%2$s" + "%1$s 제거됨 %2$s : %3$s" + "%1$s님이 %2$s에게 초대를 보냄" + "%1$s님에게 초대를 보냄" + "%1$s님이 %2$s의 초대를 회수함" + "%1$s의 초대를 회수함" + "%1$s님이 주제를 %2$s으로 변경했습니다." + "주제 변경함: %1$s" + "%1$s님이 방 주제를 삭제함" + "방 주제를 삭제함" + "%1$s님이 %2$s님의 차단을 해제함" + "%1$s님의 차단을 해제함" + "%1$s님이 멤버십에 알려지지 않은 변경 사항을 만들었습니다." + diff --git a/libraries/eventformatter/impl/src/main/res/values-pt-rBR/translations.xml b/libraries/eventformatter/impl/src/main/res/values-pt-rBR/translations.xml index f1ff1bd029..c5b36c4b1a 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 @@ -20,7 +20,7 @@ "%1$s baniu %2$s" "Você baniu %1$s" "Você baniu %1$s: %2$s" - "%1$s banido %2$s: %3$s" + "%1$s baniu %2$s: %3$s" "%1$s criou a sala" "Você criou a sala" "%1$s convidou %2$s" @@ -31,12 +31,12 @@ "%1$s entrou na sala" "Você entrou na sala" "%1$s solicitou entrada" - "%1$sconcedeu acesso a %2$s" + "%1$s concedeu o acesso a %2$s" "Você permitiu que o %1$s entrasse" - "Você solicitou entrada" + "Você pediu para entrar" "%1$s rejeitou a solicitação de %2$s para entrar" "Você rejeitou a solicitação de %1$s para entrar" - "%1$s rejeitou sua solicitação para entrar" + "%1$s rejeitou seu pedido para entrar" "%1$s não está mais interessado em entrar" "Você cancelou seu pedido para entrar" "%1$s saiu da sala" @@ -51,14 +51,14 @@ "Você alterou as mensagens fixadas" "%1$s fixou uma mensagem" "Você fixou uma mensagem" - "%1$s desfixou uma mensagem" + "%1$s desafixou 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 removeu %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-ro/translations.xml b/libraries/eventformatter/impl/src/main/res/values-ro/translations.xml index 21974de8a6..6465d3299a 100644 --- a/libraries/eventformatter/impl/src/main/res/values-ro/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-ro/translations.xml @@ -7,8 +7,8 @@ "%1$s a fost retrogradat la funcția de moderator" "%1$s și-a schimbat numele din %2$s în %3$s" "V-ați schimbat numele din %1$s în %2$s" - "%1$s și-a sters numele (era %2$s)" - "V-ați sters numele (era %1$s)" + "%1$s și-a șters numele (era %2$s)" + "V-ați șters numele (era %1$s)" "%1$s și-a schimbat numele %2$s" "V-ați schimbat numele în %1$s" "%1$s a fost promovat în funcția de administrator" @@ -19,6 +19,8 @@ "Ați șters avatarul camerei" "%1$s a adăugat o interdicție pentru %2$s" "Ați adăugat o interdicție pentru %1$s" + "L-ați interzis pe %1$s: %2$s" + "%1$s a interzis pe %2$s: %3$s" "%1$s a creat camera" "Ați creat camera" "%1$s l-a invitat pe %2$s" @@ -28,12 +30,12 @@ "%1$s v-a invitat" "%1$s a intrat în cameră" "Ați intrat în cameră" - "%1$s a solicitat să se alăture camerei" + "%1$s a cerut să se alăture camerei" "%1$s i-a permis accesul lui %2$s" "I-ați permis lui %1$s să se alăture" - "Ați solicitat să vă alăturați camerei" - "%1$s a respins solicitarea de alăturare a lui %2$s" - "Ați respins solicitarea de alăturare a lui %1$s" + "Ați cerut să vă alăturați camerei" + "%1$s a respins cererea de alăturare a lui %2$s" + "Ați respins cererea de alăturare a lui %1$s" "%1$s a respins cererea dumneavoastră de alăturare" "%1$s nu mai este interesat să se alăture camerei" "Ați anulat cererea de alăturare" @@ -41,7 +43,7 @@ "Ați părăsit camera" "%1$s a schimbat numele camerei în: %2$s" "Ați schimbat numele camerei în: %1$s" - "%1$s a sters numele camerei" + "%1$s a șters numele camerei" "Ați șters numele camerei" "%1$s nu a făcut nicio modificare" "Nu ați făcut nicio modificare" @@ -55,6 +57,8 @@ "Ați respins invitația" "%1$s l-a îndepărtat pe %2$s" "L-ați îndepărtat pe %1$s" + "L-ați îndepărtat pe %1$s: %2$s" + "%1$s l-a îndepărtat pe %2$s: %3$s" "%1$s a trimis o invitație către %2$s pentru a se alătura camerei" "Ați trimis o invitație către %1$s pentru a se alătura camerei" "%1$s a revocat invitația pentru %2$s de a se alătura camerei" 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 aab758a1a1..88a8186fec 100644 --- a/libraries/eventformatter/impl/src/main/res/values-uz/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-uz/translations.xml @@ -3,12 +3,16 @@ "(avatar ham o\'zgartirildi)" "%1$s avatarini o\'zgartirdi" "Siz avataringizni o\'zgartirdingiz" + "%1$s oddiy a’zo lavozimiga tushirildi" + "%1$s moderator lavozimiga tushirildi" "%1$s ko\'rsatiladigan nomini %2$sdan %3$sga o\'zgartirdi" "Siz ko\'rsatiladigan nomingizni %1$s dan %2$s ga o\'zgartirdingiz" "%1$s ko\'rinadigan nomini o\'chirib tashladi (avval %2$s bo\'lgan edi)" "Siz ko\'rinadigan nomingizni o\'chirib tashladingiz (avval %1$s bo\'lgan edi)" "%1$s ularning ko\'rsatiladigan nomini o\'rnating %2$s" "Siz ko\'rsatiladigan nomingizni o\'rnating %1$s" + "%1$s admin lavozimiga koʻtarildi" + "%1$s moderatorlikka ko‘tarildi" "%1$s xonani avatarini o\'zgartirdi" "Siz xonani avatarini o\'zgartirdingiz" "%1$s xonani avatarini o\'chirib tashladi" @@ -39,6 +43,14 @@ "Siz xonani nomini %1$s ga o\'zgartirdingiz" "%1$s xonani nomini o\'chirib tashladi" "Siz xonani nomini o\'chirib tashladingiz" + "%1$shech qanday o'zgarishlar qilmadi" + "Hech qanday o‘zgartirish kiritilmadi" + "%1$s qadalgan xabarlarni tahrirladi" + "Qadalgan xabarlarni o‘zgartirdingiz" + "%1$s xabarni qadadi" + "Siz xabarni qadadingiz" + "%1$s xabarni uzdi" + "Siz xabarni uzdingiz" "%1$staklifni rad etdi" "Siz taklifni rad etdingiz" "%1$s o\'chirildi %2$s" 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 index ee56fcf2c5..8fb4ac0bf4 100644 --- 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 @@ -14,7 +14,6 @@ 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.EventThreadInfo 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 @@ -102,7 +101,7 @@ class DefaultBaseRoomLastMessageFormatterTest { 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)" + val expectedBody = someoneElseId.value + ": Sticker (a sticker body)" assertThat(result.toString()).isEqualTo(expectedBody) } @@ -175,7 +174,7 @@ class DefaultBaseRoomLastMessageFormatterTest { ) { val body = "Shared body" fun createMessageContent(type: MessageType): MessageContent { - return MessageContent(body, null, false, EventThreadInfo(null, null), type) + return MessageContent(body, null, false, null, type) } val sharedContentMessagesTypes = arrayOf( 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 349cd2585f..442357f386 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 @@ -14,7 +14,6 @@ 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.EventThreadInfo 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 @@ -130,7 +129,7 @@ class DefaultPinnedMessagesBannerFormatterTest { fun `Message contents`() { val body = "Shared body" fun createMessageContent(type: MessageType): MessageContent { - return MessageContent(body, null, false, EventThreadInfo(null, null), type) + return MessageContent(body, null, false, null, type) } val sharedContentMessagesTypes = arrayOf( 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 7c87a6e034..96c452a790 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 @@ -71,7 +71,7 @@ enum class FeatureFlags( Space( key = "feature.space", title = "Spaces", - description = "Spaces are under active development, only developers should enable this flog for now.", + description = "Spaces are under active development, only developers should enable this flag for now.", defaultValue = { false }, isFinished = false, ), @@ -93,11 +93,19 @@ enum class FeatureFlags( // False so it's displayed in the developer options screen isFinished = false, ), - HideThreadedEvents( + Threads( key = "feature.thread_timeline", title = "Threads", description = "Renders thread messages as a dedicated timeline. Restarting the app is required for this setting to fully take effect.", defaultValue = { false }, isFinished = false, - ) + ), + MultiAccount( + key = "feature.multi_account", + title = "Multi accounts", + description = "Allow the application to connect to multiple accounts at the same time." + + "\n\nWARNING: this feature is EXPERIMENTAL and UNSTABLE.", + defaultValue = { false }, + isFinished = false, + ), } diff --git a/libraries/featureflag/impl/build.gradle.kts b/libraries/featureflag/impl/build.gradle.kts index c1da63dba3..c54f95a293 100644 --- a/libraries/featureflag/impl/build.gradle.kts +++ b/libraries/featureflag/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -16,19 +17,18 @@ android { namespace = "io.element.android.libraries.featureflag.impl" } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.libraries.featureflag.api) - implementation(libs.dagger) implementation(libs.androidx.datastore.preferences) implementation(projects.appconfig) implementation(projects.libraries.di) + implementation(projects.libraries.androidutils) implementation(projects.libraries.core) + implementation(projects.libraries.preferences.api) implementation(libs.coroutines.core) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) } diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt index 21f1f1c2e0..5bcbe93085 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt @@ -7,19 +7,20 @@ package io.element.android.libraries.featureflag.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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.featureflag.api.Feature import io.element.android.libraries.featureflag.api.FeatureFlagService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -import javax.inject.Inject @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) -class DefaultFeatureFlagService @Inject constructor( +@Inject +class DefaultFeatureFlagService( private val providers: Set<@JvmSuppressWildcards FeatureFlagProvider>, private val buildMeta: BuildMeta, ) : FeatureFlagService { diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt index 0c1f1a45e8..f477d2ea0d 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt @@ -7,30 +7,25 @@ package io.element.android.libraries.featureflag.impl -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.preferencesDataStore +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.featureflag.api.Feature +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import javax.inject.Inject - -private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_featureflag") /** * Note: this will be used only in the nightly and in the debug build. */ -class PreferencesFeatureFlagProvider @Inject constructor( - @ApplicationContext context: Context, +@Inject +class PreferencesFeatureFlagProvider( private val buildMeta: BuildMeta, + preferenceDataStoreFactory: PreferenceDataStoreFactory, ) : MutableFeatureFlagProvider { - private val store = context.dataStore + private val store = preferenceDataStoreFactory.create("elementx_featureflag") override val priority = MEDIUM_PRIORITY diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt index d39ef96e92..137d2d3ab1 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt @@ -7,15 +7,15 @@ package io.element.android.libraries.featureflag.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides -import dagger.multibindings.ElementsIntoSet -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.ElementsIntoSet +import dev.zacsweers.metro.Provides import io.element.android.libraries.featureflag.impl.FeatureFlagProvider import io.element.android.libraries.featureflag.impl.PreferencesFeatureFlagProvider -@Module +@BindingContainer @ContributesTo(AppScope::class) object FeatureFlagModule { @JvmStatic diff --git a/libraries/featureflag/ui/build.gradle.kts b/libraries/featureflag/ui/build.gradle.kts index 9d53b5fe07..32fd4f6eab 100644 --- a/libraries/featureflag/ui/build.gradle.kts +++ b/libraries/featureflag/ui/build.gradle.kts @@ -1,5 +1,3 @@ -import extension.setupAnvil - /* * Copyright 2023, 2024 New Vector Ltd. * @@ -16,8 +14,6 @@ android { namespace = "io.element.android.libraries.featureflag.ui" } -setupAnvil() - dependencies { implementation(projects.libraries.designsystem) } diff --git a/libraries/fullscreenintent/impl/build.gradle.kts b/libraries/fullscreenintent/impl/build.gradle.kts index 37fbd01aca..2ccb7f80b8 100644 --- a/libraries/fullscreenintent/impl/build.gradle.kts +++ b/libraries/fullscreenintent/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2024 New Vector Ltd. @@ -15,7 +16,7 @@ android { namespace = "io.element.android.libraries.fullscreenintent.impl" } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.libraries.fullscreenintent.api) @@ -27,19 +28,10 @@ dependencies { implementation(projects.services.toolbox.api) implementation(libs.androidx.datastore.preferences) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(projects.tests.testutils) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.testtags) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.mockk) - testImplementation(libs.androidx.compose.ui.test.junit) testImplementation(projects.services.toolbox.test) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } 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 14cc438f41..b05aad303c 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 @@ -19,10 +19,11 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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 @@ -30,10 +31,10 @@ import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject @SingleIn(AppScope::class) -class FullScreenIntentPermissionsPresenter @Inject constructor( +@Inject +class FullScreenIntentPermissionsPresenter( private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, private val externalIntentLauncher: ExternalIntentLauncher, private val buildMeta: BuildMeta, diff --git a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/di/FullScreenIntentModule.kt b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/di/FullScreenIntentModule.kt index 363d1db814..f1c6f1efe4 100644 --- a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/di/FullScreenIntentModule.kt +++ b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/di/FullScreenIntentModule.kt @@ -7,16 +7,16 @@ package io.element.android.libraries.fullscreenintent.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.di.AppScope import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.fullscreenintent.impl.FullScreenIntentPermissionsPresenter @ContributesTo(AppScope::class) -@Module +@BindingContainer interface FullScreenIntentModule { @Binds fun bindFullScreenIntentPermissionsPresenter(presenter: FullScreenIntentPermissionsPresenter): Presenter diff --git a/libraries/indicator/impl/build.gradle.kts b/libraries/indicator/impl/build.gradle.kts index d5046eb00c..2596b9fe67 100644 --- a/libraries/indicator/impl/build.gradle.kts +++ b/libraries/indicator/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -11,14 +12,13 @@ plugins { id("io.element.android-compose-library") } -setupAnvil() +setupDependencyInjection() android { namespace = "io.element.android.libraries.indicator.impl" } dependencies { - implementation(libs.dagger) implementation(projects.libraries.di) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.matrix.api) @@ -27,11 +27,7 @@ dependencies { api(projects.libraries.indicator.api) + testCommonDependencies(libs) testImplementation(projects.libraries.featureflag.test) 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/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt b/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt index 20f23f907e..5cd17418b3 100644 --- a/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt +++ b/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt @@ -13,17 +13,18 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.di.SessionScope import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.matrix.api.encryption.BackupState 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 javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultIndicatorService @Inject constructor( +@Inject +class DefaultIndicatorService( private val sessionVerificationService: SessionVerificationService, private val encryptionService: EncryptionService, ) : IndicatorService { diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt index d3f9cf5971..b5b46ca847 100644 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt @@ -31,9 +31,8 @@ import org.maplibre.android.maps.Projection */ @Composable public inline fun rememberCameraPositionState( - key: String? = null, crossinline init: CameraPositionState.() -> Unit = {} -): CameraPositionState = rememberSaveable(key = key, saver = CameraPositionState.Saver) { +): CameraPositionState = rememberSaveable(saver = CameraPositionState.Saver) { CameraPositionState().apply(init) } diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLibreMap.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLibreMap.kt index 5de213615e..f1ef0f0459 100644 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLibreMap.kt +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLibreMap.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.maplibre.compose -import android.content.ComponentCallbacks +import android.content.ComponentCallbacks2 import android.content.Context import android.content.res.Configuration import android.os.Bundle @@ -235,11 +235,15 @@ private fun MapView.lifecycleObserver(previousState: MutableState val roomListService: RoomListService + val spaceService: SpaceService val mediaLoader: MatrixMediaLoader val sessionCoroutineScope: CoroutineScope val ignoredUsersFlow: StateFlow> @@ -154,11 +156,6 @@ interface MatrixClient { */ suspend fun currentSlidingSyncVersion(): Result - /** - * Returns the available sliding sync versions for the current user. - */ - suspend fun availableSlidingSyncVersions(): Result> - fun canDeactivateAccount(): Boolean suspend fun deactivateAccount(password: String, eraseData: Boolean): Result @@ -176,6 +173,16 @@ interface MatrixClient { * Returns the maximum file upload size allowed by the Matrix server. */ suspend fun getMaxFileUploadSize(): Result + + /** + * Returns the list of shared recent emoji reactions for this account. + */ + suspend fun getRecentEmojis(): Result> + + /** + * Adds an emoji to the list of recent emoji reactions for this account. + */ + suspend fun addRecentEmoji(emoji: String): Result } /** 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 ef73edfaf5..03e8d57150 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 @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.api.auth sealed class AuthenticationException(message: String) : Exception(message) { + class AccountAlreadyLoggedIn(userId: String) : AuthenticationException(userId) class InvalidServerName(message: String) : AuthenticationException(message) class SlidingSyncVersion(message: String) : AuthenticationException(message) class Oidc(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 d1c47ae663..38777944ee 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 @@ -13,14 +13,9 @@ import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.sessionstorage.api.LoggedInState -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow interface MatrixAuthenticationService { - fun loggedInStateFlow(): Flow - suspend fun getLatestSessionId(): SessionId? - /** * Restore a session from a [sessionId]. * Do not restore anything it the access token is not valid anymore. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt index a44e00b664..26a030d361 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt @@ -151,7 +151,7 @@ object MatrixPatterns { val urlMatch = match.groupValues[1] when (val permalink = permalinkParser.parse(urlMatch)) { is PermalinkData.UserLink -> { - add(MatrixPatternResult(MatrixPatternType.USER_ID, permalink.userId.toString(), match.range.first, match.range.last + 1)) + add(MatrixPatternResult(MatrixPatternType.USER_ID, permalink.userId.value, match.range.first, match.range.last + 1)) } is PermalinkData.RoomLink -> { when (permalink.roomIdOrAlias) { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/mxc/MxcTools.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/mxc/MxcTools.kt index 7eb3285f74..8525425e1b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/mxc/MxcTools.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/mxc/MxcTools.kt @@ -7,9 +7,10 @@ package io.element.android.libraries.matrix.api.mxc -import javax.inject.Inject +import dev.zacsweers.metro.Inject -class MxcTools @Inject constructor() { +@Inject +class MxcTools { /** * Regex to match a Matrix Content (mxc://) URI. * 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 338193ed44..1a58ced0fd 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 @@ -49,9 +49,10 @@ sealed interface NotificationContent { val senderId: UserId, ) : MessageLike - data class CallNotify( + data class RtcNotification( val senderId: UserId, - val type: CallNotifyType, + val type: RtcNotificationType, + val expirationTimestampMillis: Long ) : MessageLike data object CallHangup : MessageLike @@ -118,7 +119,7 @@ sealed interface NotificationContent { ) : NotificationContent } -enum class CallNotifyType { +enum class RtcNotificationType { RING, NOTIFY } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkData.kt index 1f9dd8af8d..1f5f39dee7 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkData.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.api.permalink import android.net.Uri +import android.os.Parcelable import androidx.compose.runtime.Immutable import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -15,13 +16,15 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.UserId import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.parcelize.Parcelize /** * This sealed class represents all the permalink cases. * You don't have to instantiate yourself but should use [PermalinkParser] instead. */ @Immutable -sealed interface PermalinkData { +@Parcelize +sealed interface PermalinkData : Parcelable { data class RoomLink( val roomIdOrAlias: RoomIdOrAlias, val eventId: EventId? = null, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/AddRecentEmoji.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/AddRecentEmoji.kt new file mode 100644 index 0000000000..da657ea78a --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/AddRecentEmoji.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.matrix.api.recentemojis + +import dev.zacsweers.metro.Inject +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.MatrixClient +import kotlinx.coroutines.withContext + +@Inject +class AddRecentEmoji( + private val client: MatrixClient, + private val dispatchers: CoroutineDispatchers, +) { + suspend operator fun invoke(emoji: String): Result = withContext(dispatchers.io) { + client.addRecentEmoji(emoji) + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/GetRecentEmojis.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/GetRecentEmojis.kt new file mode 100644 index 0000000000..53adf88c37 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/GetRecentEmojis.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.recentemojis + +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import kotlinx.coroutines.withContext + +fun interface GetRecentEmojis { + suspend operator fun invoke(): Result> +} + +@ContributesBinding(SessionScope::class) +@Inject +class DefaultGetRecentEmojis( + private val client: MatrixClient, + private val dispatchers: CoroutineDispatchers, +) : GetRecentEmojis { + override suspend operator fun invoke(): Result> = withContext(dispatchers.io) { + client.getRecentEmojis() + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt index 7e902a66fa..2694191f89 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt @@ -18,6 +18,7 @@ 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.Flow import kotlinx.coroutines.flow.StateFlow import java.io.Closeable @@ -239,6 +240,12 @@ interface BaseRoom : Closeable { */ suspend fun reportRoom(reason: String?): Result + suspend fun declineCall(notificationEventId: EventId): Result + + suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow + + suspend fun threadRootIdForEvent(eventId: EventId): Result + /** * Destroy the room and release all resources associated to it. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MessageEventType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MessageEventType.kt index ce98ecfe6b..0f2a61da6e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MessageEventType.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MessageEventType.kt @@ -7,28 +7,32 @@ package io.element.android.libraries.matrix.api.room -enum class MessageEventType { - CALL_ANSWER, - CALL_INVITE, - CALL_HANGUP, - CALL_CANDIDATES, - CALL_NOTIFY, - KEY_VERIFICATION_READY, - KEY_VERIFICATION_START, - KEY_VERIFICATION_CANCEL, - KEY_VERIFICATION_ACCEPT, - KEY_VERIFICATION_KEY, - KEY_VERIFICATION_MAC, - KEY_VERIFICATION_DONE, - REACTION, - ROOM_ENCRYPTED, - ROOM_MESSAGE, - ROOM_REDACTION, - STICKER, - POLL_END, - POLL_RESPONSE, - POLL_START, - UNSTABLE_POLL_END, - UNSTABLE_POLL_RESPONSE, - UNSTABLE_POLL_START, +import androidx.compose.runtime.Immutable + +@Immutable +sealed interface MessageEventType { + data object CallAnswer : MessageEventType + data object CallInvite : MessageEventType + data object CallHangup : MessageEventType + data object CallCandidates : MessageEventType + data object RtcNotification : MessageEventType + data object KeyVerificationReady : MessageEventType + data object KeyVerificationStart : MessageEventType + data object KeyVerificationCancel : MessageEventType + data object KeyVerificationAccept : MessageEventType + data object KeyVerificationKey : MessageEventType + data object KeyVerificationMac : MessageEventType + data object KeyVerificationDone : MessageEventType + data object Reaction : MessageEventType + data object RoomEncrypted : MessageEventType + data object RoomMessage : MessageEventType + data object RoomRedaction : MessageEventType + data object Sticker : MessageEventType + data object PollEnd : MessageEventType + data object PollResponse : MessageEventType + data object PollStart : MessageEventType + data object UnstablePollEnd : MessageEventType + data object UnstablePollResponse : MessageEventType + data object UnstablePollStart : MessageEventType + data class Other(val type: String) : MessageEventType } 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 c2db1e9ec5..f1b1104a27 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 @@ -17,7 +17,6 @@ data class RoomMember( val membership: RoomMembershipState, val isNameAmbiguous: Boolean, val powerLevel: Long, - val normalizedPowerLevel: Long, val isIgnored: Boolean, val role: Role, val membershipChangeReason: String?, 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 19d7fdaaf2..a4a718b219 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 @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.asSharedFlow class RoomMembershipObserver { data class RoomMembershipUpdate( val roomId: RoomId, + val isSpace: Boolean, val isUserInRoom: Boolean, val change: MembershipChange, ) @@ -22,12 +23,23 @@ class RoomMembershipObserver { private val _updates = MutableSharedFlow(extraBufferCapacity = 10) val updates = _updates.asSharedFlow() - suspend fun notifyUserLeftRoom(roomId: RoomId, membershipBeforeLeft: CurrentUserMembership) { + suspend fun notifyUserLeftRoom( + roomId: RoomId, + isSpace: Boolean, + 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)) + _updates.emit( + RoomMembershipUpdate( + roomId = roomId, + isSpace = isSpace, + isUserInRoom = false, + change = membershipChange, + ) + ) } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt index 6fe33d242a..4dfc3ac565 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt @@ -7,8 +7,10 @@ package io.element.android.libraries.matrix.api.room.join +import androidx.compose.runtime.Immutable import io.element.android.libraries.matrix.api.core.RoomId +@Immutable sealed interface AllowRule { data class RoomMembership(val roomId: RoomId) : AllowRule data class Custom(val json: String) : AllowRule diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt index f733cb56ac..0eada82e29 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt @@ -7,12 +7,16 @@ package io.element.android.libraries.matrix.api.room.join +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable sealed interface JoinRule { data object Public : JoinRule data object Private : JoinRule data object Knock : JoinRule data object Invite : JoinRule - data class Restricted(val rules: List) : JoinRule - data class KnockRestricted(val rules: List) : JoinRule + data class Restricted(val rules: ImmutableList) : JoinRule + data class KnockRestricted(val rules: ImmutableList) : JoinRule data class Custom(val value: String) : JoinRule } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt new file mode 100644 index 0000000000..1e89fd2e98 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.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.matrix.api.spaces + +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 +import io.element.android.libraries.matrix.api.user.MatrixUser + +data class SpaceRoom( + val name: String?, + val avatarUrl: String?, + val canonicalAlias: RoomAlias?, + val childrenCount: Int, + val guestCanJoin: Boolean, + val heroes: List, + val joinRule: JoinRule?, + val numJoinedMembers: Int, + val roomId: RoomId, + val roomType: RoomType, + val state: CurrentUserMembership?, + val topic: String?, + val worldReadable: Boolean, + /** + * The via parameters of the room. + */ + val via: List, +) { + val isSpace = roomType == RoomType.Space +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.kt new file mode 100644 index 0000000000..1dddfadc5b --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.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.spaces + +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import java.util.Optional + +interface SpaceRoomList { + sealed interface PaginationStatus { + data object Loading : PaginationStatus + data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus + } + + val roomId: RoomId + + val currentSpaceFlow: StateFlow> + + val spaceRoomsFlow: Flow> + val paginationStatusFlow: StateFlow + suspend fun paginate(): Result + + fun destroy() +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt new file mode 100644 index 0000000000..b4572ad0bb --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.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.api.spaces + +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.coroutines.flow.SharedFlow + +interface SpaceService { + val spaceRoomsFlow: SharedFlow> + suspend fun joinedSpaces(): Result> + + fun spaceRoomList(id: RoomId): SpaceRoomList +} 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 4eceeac4da..f8f5793368 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 @@ -151,7 +151,7 @@ interface Timeline : AutoCloseable { suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result - suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result + suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result suspend fun forwardEvent(eventId: EventId, roomIds: List): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/ThreadSummary.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/ThreadSummary.kt index 4960330448..c34e4c9ac3 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/ThreadSummary.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/ThreadSummary.kt @@ -14,10 +14,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails -data class EventThreadInfo( - val threadRootId: ThreadId?, - val threadSummary: ThreadSummary?, -) +sealed interface EventThreadInfo { + data class ThreadRoot(val summary: ThreadSummary) : EventThreadInfo + data class ThreadResponse(val threadRootId: ThreadId) : EventThreadInfo +} data class ThreadSummary( val latestEvent: AsyncData, 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 a6bea83565..5fefe8ae31 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 @@ -24,7 +24,7 @@ data class MessageContent( val body: String, val inReplyTo: InReplyTo?, val isEdited: Boolean, - val threadInfo: EventThreadInfo, + val threadInfo: EventThreadInfo?, val type: MessageType ) : EventContent diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt index 41d0dc7483..d6b354376c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt @@ -15,5 +15,6 @@ object EventType { // Call Events const val CALL_INVITE = "m.call.invite" - const val CALL_NOTIFY = "m.call.notify" + + const val RTC_NOTIFICATION = "org.matrix.msc4075.rtc.notification" } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentSessionIdHolder.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentSessionIdHolder.kt deleted file mode 100644 index a18d0c2fb7..0000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentSessionIdHolder.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.libraries.matrix.api.user - -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.SessionId -import javax.inject.Inject - -@SingleIn(SessionScope::class) -class CurrentSessionIdHolder @Inject constructor(matrixClient: MatrixClient) { - val current = matrixClient.sessionId - - fun isCurrentSession(sessionId: SessionId?): Boolean = current == sessionId -} 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 897e17c611..edf8c6ff78 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 @@ -10,20 +10,14 @@ package io.element.android.libraries.matrix.api.verification import android.os.Parcelable 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.user.MatrixUser import kotlinx.parcelize.Parcelize @Parcelize data class SessionVerificationRequestDetails( - val senderProfile: SenderProfile, + val senderProfile: MatrixUser, val flowId: FlowId, val deviceId: DeviceId, + val deviceDisplayName: String?, val firstSeenTimestamp: Long, -) : Parcelable { - @Parcelize - data class SenderProfile( - val userId: UserId, - val displayName: String?, - val avatarUrl: String?, - ) : Parcelable -} +) : Parcelable 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 7a735bbf92..8334fb5fde 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 @@ -15,5 +15,6 @@ interface CallWidgetSettingsProvider { widgetId: String = UUID.randomUUID().toString(), encrypted: Boolean, direct: Boolean, + hasActiveCall: Boolean, ): MatrixWidgetSettings } diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 790d23ec07..14ba9df71f 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -16,7 +17,7 @@ android { namespace = "io.element.android.libraries.matrix.impl" } -setupAnvil() +setupDependencyInjection() dependencies { releaseImplementation(libs.matrix.sdk) @@ -35,24 +36,18 @@ dependencies { implementation(projects.services.analytics.api) implementation(projects.services.toolbox.api) api(projects.libraries.matrix.api) - implementation(libs.dagger) implementation(projects.libraries.core) - implementation("net.java.dev.jna:jna:5.17.0@aar") + implementation("net.java.dev.jna:jna:5.18.1@aar") implementation(libs.androidx.datastore.preferences) implementation(libs.serialization.json) implementation(libs.kotlinx.collections.immutable) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.preferences.test) - testImplementation(projects.libraries.sessionStorage.implMemory) + testImplementation(projects.libraries.previewutils) testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.services.analytics.test) testImplementation(projects.services.toolbox.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.turbine) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/ClientBuilderProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/ClientBuilderProvider.kt index 51c9b4c380..57486da3e8 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/ClientBuilderProvider.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/ClientBuilderProvider.kt @@ -7,17 +7,18 @@ package io.element.android.libraries.matrix.impl -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import org.matrix.rustcomponents.sdk.ClientBuilder -import javax.inject.Inject interface ClientBuilderProvider { fun provide(): ClientBuilder } @ContributesBinding(AppScope::class) -class RustClientBuilderProvider @Inject constructor() : ClientBuilderProvider { +@Inject +class RustClientBuilderProvider : ClientBuilderProvider { override fun provide(): ClientBuilder { return ClientBuilder() } 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 85c1dde7e2..3f84621cb7 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 @@ -42,6 +42,7 @@ import io.element.android.libraries.matrix.api.room.join.JoinRule 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.spaces.SpaceService 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,6 +51,7 @@ 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.encryption.RustEncryptionService import io.element.android.libraries.matrix.impl.exception.mapClientException +import io.element.android.libraries.matrix.impl.mapper.map 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 @@ -71,9 +73,9 @@ 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.spaces.RustSpaceService import io.element.android.libraries.matrix.impl.sync.RustSyncService import io.element.android.libraries.matrix.impl.sync.map -import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper import io.element.android.libraries.matrix.impl.util.SessionPathsProvider import io.element.android.libraries.matrix.impl.util.cancelAndDestroy @@ -143,6 +145,7 @@ class RustMatrixClient( private val sessionDispatcher = dispatchers.io.limitedParallelism(64) private val innerRoomListService = innerSyncService.roomListService() + private val innerSpaceService = innerClient.spaceService() private val rustSyncService = RustSyncService( inner = innerSyncService, @@ -184,6 +187,12 @@ class RustMatrixClient( roomSyncSubscriber = roomSyncSubscriber, ) + override val spaceService: SpaceService = RustSpaceService( + innerSpaceService = innerSpaceService, + sessionCoroutineScope = sessionCoroutineScope, + sessionDispatcher = sessionDispatcher, + ) + private val verificationService = RustSessionVerificationService( client = innerClient, isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running }, @@ -226,7 +235,6 @@ class RustMatrixClient( private val _userProfile: MutableStateFlow = MutableStateFlow( MatrixUser( userId = sessionId, - // TODO cache for displayName? displayName = null, avatarUrl = null, ) @@ -255,6 +263,16 @@ class RustMatrixClient( // Start notification settings notificationSettingsService.start() + // Update the user profile in the session store if needed + sessionStore.getSession(sessionId.value)?.let { sessionData -> + _userProfile.emit( + MatrixUser( + userId = sessionId, + displayName = sessionData.userDisplayName, + avatarUrl = sessionData.userAvatarUrl, + ) + ) + } // Force a refresh of the profile getUserProfile() } @@ -278,7 +296,6 @@ class RustMatrixClient( } override suspend fun getRoom(roomId: RoomId): BaseRoom? = withContext(sessionDispatcher) { - innerClient.rooms() roomFactory.getBaseRoom(roomId) } @@ -386,12 +403,20 @@ class RustMatrixClient( override suspend fun getProfile(userId: UserId): Result = withContext(sessionDispatcher) { runCatchingExceptions { - innerClient.getProfile(userId.value).let(UserProfileMapper::map) + innerClient.getProfile(userId.value).map() } } override suspend fun getUserProfile(): Result = getProfile(sessionId) - .onSuccess { _userProfile.tryEmit(it) } + .onSuccess { matrixUser -> + _userProfile.emit(matrixUser) + // Also update our session storage + sessionStore.updateUserProfile( + sessionId = sessionId.value, + displayName = matrixUser.displayName, + avatarUrl = matrixUser.avatarUrl, + ) + } override suspend fun searchUsers(searchTerm: String, limit: Long): Result = withContext(sessionDispatcher) { @@ -540,6 +565,7 @@ class RustMatrixClient( sessionDelegate.clearCurrentClient() innerRoomListService.close() + innerSpaceService.close() notificationService.close() encryptionService.close() innerClient.close() @@ -679,12 +705,6 @@ class RustMatrixClient( }) }.buffer(Channel.UNLIMITED) - override suspend fun availableSlidingSyncVersions(): Result> = withContext(sessionDispatcher) { - runCatchingExceptions { - innerClient.availableSlidingSyncVersions().map { it.map() } - } - } - override suspend fun currentSlidingSyncVersion(): Result = withContext(sessionDispatcher) { runCatchingExceptions { innerClient.session().slidingSyncVersion.map() @@ -705,6 +725,18 @@ class RustMatrixClient( runCatchingExceptions { innerClient.getMaxMediaUploadSize().toLong() } } + override suspend fun addRecentEmoji(emoji: String): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + innerClient.addRecentEmoji(emoji) + } + } + + override suspend fun getRecentEmojis(): Result> = withContext(sessionDispatcher) { + runCatchingExceptions { + innerClient.getRecentEmojis().map { it.emoji } + } + } + private suspend fun File.getCacheSize( includeCryptoDb: Boolean = false, ): Long = withContext(sessionDispatcher) { 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 4ad26800cd..572bcbfd18 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 @@ -7,7 +7,9 @@ package io.element.android.libraries.matrix.impl +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.BaseDirectory import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -28,6 +30,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientBuilder +import org.matrix.rustcomponents.sdk.RequestConfig import org.matrix.rustcomponents.sdk.Session import org.matrix.rustcomponents.sdk.SlidingSyncVersion import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder @@ -37,10 +40,10 @@ 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 -class RustMatrixClientFactory @Inject constructor( - private val baseDirectory: File, +@Inject +class RustMatrixClientFactory( + @BaseDirectory private val baseDirectory: File, @CacheDirectory private val cacheDirectory: File, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, @@ -132,7 +135,14 @@ class RustMatrixClientFactory @Inject constructor( ) ) .enableShareHistoryOnInvite(featureFlagService.isFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite)) - .threadsEnabled(featureFlagService.isFeatureEnabled(FeatureFlags.HideThreadedEvents), threadSubscriptions = false) + .threadsEnabled(featureFlagService.isFeatureEnabled(FeatureFlags.Threads), threadSubscriptions = false) + .requestConfig(RequestConfig( + timeout = 30_000uL, + retryLimit = 0u, + // Use default values for the rest + maxConcurrentRequests = null, + maxRetryTime = null, + )) .run { // Apply sliding sync version settings when (slidingSyncType) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustSdkMetadata.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustSdkMetadata.kt index b5dc6e27bf..88ff69459f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustSdkMetadata.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustSdkMetadata.kt @@ -7,14 +7,15 @@ package io.element.android.libraries.matrix.impl -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.SdkMetadata import org.matrix.rustcomponents.sdk.sdkGitSha -import javax.inject.Inject @ContributesBinding(AppScope::class) -class RustSdkMetadata @Inject constructor() : SdkMetadata { +@Inject +class RustSdkMetadata : SdkMetadata { override val sdkGitSha: String get() = sdkGitSha() } 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 7175913dad..05eb4c4d5f 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 @@ -14,6 +14,7 @@ import org.matrix.rustcomponents.sdk.OidcException fun Throwable.mapAuthenticationException(): AuthenticationException { val message = this.message ?: "Unknown error" return when (this) { + is AuthenticationException -> this is ClientBuildException -> when (this) { is ClientBuildException.Generic -> AuthenticationException.Generic(message) is ClientBuildException.InvalidServerName -> AuthenticationException.InvalidServerName(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 ea398f935d..a614c97680 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,13 +7,14 @@ package io.element.android.libraries.matrix.impl.auth +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.auth.OidcConfig import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider import org.matrix.rustcomponents.sdk.OidcConfiguration -import javax.inject.Inject -class OidcConfigurationProvider @Inject constructor( +@Inject +class OidcConfigurationProvider( private val buildMeta: BuildMeta, private val oidcRedirectUrlProvider: OidcRedirectUrlProvider, ) { 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 1a044a9f4a..88c86a43d6 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 @@ -7,13 +7,15 @@ package io.element.android.libraries.matrix.impl.auth -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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 +import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.OidcDetails @@ -32,11 +34,9 @@ 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.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext @@ -49,11 +49,11 @@ import org.matrix.rustcomponents.sdk.QrLoginProgress import org.matrix.rustcomponents.sdk.QrLoginProgressListener import timber.log.Timber import uniffi.matrix_sdk.OAuthAuthorizationData -import javax.inject.Inject @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) -class RustMatrixAuthenticationService @Inject constructor( +@Inject +class RustMatrixAuthenticationService( private val sessionPathsFactory: SessionPathsFactory, private val coroutineDispatchers: CoroutineDispatchers, private val sessionStore: SessionStore, @@ -82,14 +82,6 @@ class RustMatrixAuthenticationService @Inject constructor( .also { sessionPaths = it } } - override fun loggedInStateFlow(): Flow { - return sessionStore.isLoggedIn() - } - - override suspend fun getLatestSessionId(): SessionId? = withContext(coroutineDispatchers.io) { - sessionStore.getLatestSession()?.userId?.let { SessionId(it) } - } - override suspend fun restoreSession(sessionId: SessionId): Result = withContext(coroutineDispatchers.io) { runCatchingExceptions { val sessionData = sessionStore.getSession(sessionId.value) @@ -148,6 +140,8 @@ class RustMatrixAuthenticationService @Inject constructor( 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) + // Ensure that the user is not already logged in with the same account + ensureNotAlreadyLoggedIn(client) val sessionData = client.session() .toSessionData( isTokenValid = true, @@ -157,7 +151,7 @@ class RustMatrixAuthenticationService @Inject constructor( ) val matrixClient = rustMatrixClientFactory.create(client) newMatrixClientObservers.forEach { it.invoke(matrixClient) } - sessionStore.storeData(sessionData) + sessionStore.addSession(sessionData) // Clean up the strong reference held here since it's no longer necessary currentClient = null @@ -181,7 +175,7 @@ class RustMatrixAuthenticationService @Inject constructor( sessionPaths = currentSessionPaths, ) clear() - sessionStore.storeData(sessionData) + sessionStore.addSession(sessionData) SessionId(sessionData.userId) } } @@ -236,20 +230,22 @@ class RustMatrixAuthenticationService @Inject constructor( val client = currentClient ?: error("You need to call `setHomeserver()` first") val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first") client.loginWithOidcCallback(callbackUrl) + + // Free the pending data since we won't use it to abort the flow anymore + pendingOAuthAuthorizationData?.close() + pendingOAuthAuthorizationData = null + + // Ensure that the user is not already logged in with the same account + ensureNotAlreadyLoggedIn(client) val sessionData = client.session().toSessionData( isTokenValid = true, loginType = LoginType.OIDC, passphrase = pendingPassphrase, sessionPaths = currentSessionPaths, ) - - // Free the pending data since we won't use it to abort the flow anymore - pendingOAuthAuthorizationData?.close() - pendingOAuthAuthorizationData = null - val matrixClient = rustMatrixClientFactory.create(client) newMatrixClientObservers.forEach { it.invoke(matrixClient) } - sessionStore.storeData(sessionData) + sessionStore.addSession(sessionData) // Clean up the strong reference held here since it's no longer necessary currentClient = null @@ -262,6 +258,21 @@ class RustMatrixAuthenticationService @Inject constructor( } } + @Throws(AuthenticationException.AccountAlreadyLoggedIn::class) + private suspend fun ensureNotAlreadyLoggedIn(client: Client) { + val newUserId = client.userId() + val accountAlreadyLoggedIn = sessionStore.getAllSessions().any { + it.userId == newUserId + } + if (accountAlreadyLoggedIn) { + // Sign out the client, ignoring any error + runCatchingExceptions { + client.logout() + } + throw AuthenticationException.AccountAlreadyLoggedIn(newUserId) + } + } + override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) = withContext(coroutineDispatchers.io) { val sdkQrCodeLoginData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData @@ -284,7 +295,8 @@ class RustMatrixAuthenticationService @Inject constructor( oidcConfiguration = oidcConfiguration, progressListener = progressListener, ) - + // Ensure that the user is not already logged in with the same account + ensureNotAlreadyLoggedIn(client) val sessionData = client.session() .toSessionData( isTokenValid = true, @@ -294,7 +306,7 @@ class RustMatrixAuthenticationService @Inject constructor( ) val matrixClient = rustMatrixClientFactory.create(client) newMatrixClientObservers.forEach { it.invoke(matrixClient) } - sessionStore.storeData(sessionData) + sessionStore.addSession(sessionData) // Clean up the strong reference held here since it's no longer necessary 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 367a272a75..17cff41bc1 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 @@ -7,16 +7,17 @@ package io.element.android.libraries.matrix.impl.auth.qrlogin -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 import org.matrix.rustcomponents.sdk.QrCodeData -import javax.inject.Inject @ContributesBinding(AppScope::class) -class RustQrCodeLoginDataFactory @Inject constructor() : MatrixQrCodeLoginDataFactory { +@Inject +class RustQrCodeLoginDataFactory : MatrixQrCodeLoginDataFactory { override fun parseQrCodeData(data: ByteArray): Result { return runCatchingExceptions { SdkQrCodeLoginData(QrCodeData.fromBytes(data)) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/DefaultUserCertificatesProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/DefaultUserCertificatesProvider.kt index 5e81a71c98..d5d637f54b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/DefaultUserCertificatesProvider.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/DefaultUserCertificatesProvider.kt @@ -7,15 +7,16 @@ package io.element.android.libraries.matrix.impl.certificates -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import timber.log.Timber import java.security.KeyStore import java.security.KeyStoreException -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultUserCertificatesProvider @Inject constructor() : UserCertificatesProvider { +@Inject +class DefaultUserCertificatesProvider : UserCertificatesProvider { /** * Get additional user-installed certificates from the `AndroidCAStore` `Keystore`. * 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 6a04079a7e..4d1f244bd2 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 @@ -7,12 +7,13 @@ package io.element.android.libraries.matrix.impl.di -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.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.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaPreviewService @@ -24,9 +25,14 @@ import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.verification.SessionVerificationService import kotlinx.coroutines.CoroutineScope -@Module +@BindingContainer @ContributesTo(SessionScope::class) object SessionMatrixModule { + @Provides + fun providesSessionId(matrixClient: MatrixClient): SessionId { + return matrixClient.sessionId + } + @Provides fun providesSessionVerificationService(matrixClient: MatrixClient): SessionVerificationService { return matrixClient.sessionVerificationService() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGenerator.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGenerator.kt index 941cdbb189..b6d9aa2b89 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGenerator.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGenerator.kt @@ -8,15 +8,16 @@ package io.element.android.libraries.matrix.impl.keys import android.util.Base64 -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import java.security.SecureRandom -import javax.inject.Inject private const val SECRET_SIZE = 256 @ContributesBinding(AppScope::class) -class DefaultPassphraseGenerator @Inject constructor() : PassphraseGenerator { +@Inject +class DefaultPassphraseGenerator : PassphraseGenerator { override fun generatePassphrase(): String? { val key = ByteArray(size = SECRET_SIZE) SecureRandom().nextBytes(key) 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 1d45c47470..2b5cac67ea 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 @@ -34,6 +34,11 @@ internal fun Session.toSessionData( passphrase = passphrase, sessionPath = sessionPaths.fileDirectory.absolutePath, cachePath = sessionPaths.cacheDirectory.absolutePath, + // Note: position and lastUsageIndex will be set by the SessionStore when adding the session + position = 0, + lastUsageIndex = 0, + userDisplayName = null, + userAvatarUrl = null, ) internal fun ExternalSession.toSessionData( @@ -55,4 +60,8 @@ internal fun ExternalSession.toSessionData( passphrase = passphrase, sessionPath = sessionPaths.fileDirectory.absolutePath, cachePath = sessionPaths.cacheDirectory.absolutePath, + position = 0, + lastUsageIndex = 0, + userDisplayName = null, + userAvatarUrl = null, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserProfileMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/UserProfileMapper.kt similarity index 53% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserProfileMapper.kt rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/UserProfileMapper.kt index f8f842c4be..6cc338610b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserProfileMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/UserProfileMapper.kt @@ -5,17 +5,14 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.matrix.impl.usersearch +package io.element.android.libraries.matrix.impl.mapper import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser import org.matrix.rustcomponents.sdk.UserProfile -object UserProfileMapper { - fun map(userProfile: UserProfile): MatrixUser = - MatrixUser( - userId = UserId(userProfile.userId), - displayName = userProfile.displayName, - avatarUrl = userProfile.avatarUrl, - ) -} +fun UserProfile.map() = MatrixUser( + userId = UserId(userId), + displayName = displayName, + avatarUrl = avatarUrl, +) 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 85f87b271f..2ca4a3c823 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 @@ -10,16 +10,16 @@ package io.element.android.libraries.matrix.impl.notification 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.notification.CallNotifyType import io.element.android.libraries.matrix.api.notification.NotificationContent +import io.element.android.libraries.matrix.api.notification.RtcNotificationType import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper import org.matrix.rustcomponents.sdk.MessageLikeEventContent -import org.matrix.rustcomponents.sdk.NotifyType import org.matrix.rustcomponents.sdk.StateEventContent import org.matrix.rustcomponents.sdk.TimelineEvent import org.matrix.rustcomponents.sdk.TimelineEventType import org.matrix.rustcomponents.sdk.use +import org.matrix.rustcomponents.sdk.RtcNotificationType as SdkRtcNotificationType class TimelineEventToNotificationContentMapper { fun map(timelineEvent: TimelineEvent): Result { @@ -78,7 +78,11 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon MessageLikeEventContent.CallCandidates -> NotificationContent.MessageLike.CallCandidates MessageLikeEventContent.CallHangup -> NotificationContent.MessageLike.CallHangup MessageLikeEventContent.CallInvite -> NotificationContent.MessageLike.CallInvite(senderId) - is MessageLikeEventContent.CallNotify -> NotificationContent.MessageLike.CallNotify(senderId, notifyType.map()) + is MessageLikeEventContent.RtcNotification -> NotificationContent.MessageLike.RtcNotification( + senderId = senderId, + type = notificationType.map(), + expirationTimestampMillis = expirationTs.toLong() + ) MessageLikeEventContent.KeyVerificationAccept -> NotificationContent.MessageLike.KeyVerificationAccept MessageLikeEventContent.KeyVerificationCancel -> NotificationContent.MessageLike.KeyVerificationCancel MessageLikeEventContent.KeyVerificationDone -> NotificationContent.MessageLike.KeyVerificationDone @@ -101,7 +105,7 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon } } -private fun NotifyType.map(): CallNotifyType = when (this) { - NotifyType.NOTIFY -> CallNotifyType.NOTIFY - NotifyType.RING -> CallNotifyType.RING +private fun SdkRtcNotificationType.map(): RtcNotificationType = when (this) { + SdkRtcNotificationType.NOTIFICATION -> RtcNotificationType.NOTIFY + SdkRtcNotificationType.RING -> RtcNotificationType.RING } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/paths/SessionPathsFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/paths/SessionPathsFactory.kt index e4d0ef4799..d583872503 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/paths/SessionPathsFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/paths/SessionPathsFactory.kt @@ -7,13 +7,15 @@ package io.element.android.libraries.matrix.impl.paths +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.BaseDirectory import io.element.android.libraries.di.CacheDirectory import java.io.File import java.util.UUID -import javax.inject.Inject -class SessionPathsFactory @Inject constructor( - private val baseDirectory: File, +@Inject +class SessionPathsFactory( + @BaseDirectory private val baseDirectory: File, @CacheDirectory private val cacheDirectory: File, ) { fun create(): SessionPaths { 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 9e881f18aa..c03be3bbe9 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 @@ -9,18 +9,19 @@ package io.element.android.libraries.matrix.impl.permalink import android.net.Uri import androidx.core.net.toUri -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.appconfig.MatrixConfiguration import io.element.android.libraries.core.extensions.replacePrefix -import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.permalink.MatrixToConverter -import javax.inject.Inject /** * Mapping of an input URI to a matrix.to compliant URI. */ @ContributesBinding(AppScope::class) -class DefaultMatrixToConverter @Inject constructor() : MatrixToConverter { +@Inject +class DefaultMatrixToConverter : MatrixToConverter { /** * Try to convert a URL from an element web instance or from a client permalink to a matrix.to url. * To be successfully converted, URL path should contain one of the [SUPPORTED_PATHS]. 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 eb0524886f..2ed7990ee4 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 @@ -7,9 +7,10 @@ package io.element.android.libraries.matrix.impl.permalink -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 import io.element.android.libraries.matrix.api.core.UserId @@ -17,10 +18,10 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.matrix.api.permalink.PermalinkBuilderError import org.matrix.rustcomponents.sdk.matrixToRoomAliasPermalink import org.matrix.rustcomponents.sdk.matrixToUserPermalink -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultPermalinkBuilder @Inject constructor() : PermalinkBuilder { +@Inject +class DefaultPermalinkBuilder : PermalinkBuilder { override fun permalinkForUser(userId: UserId): Result { if (!MatrixPatterns.isUserId(userId.value)) { return Result.failure(PermalinkBuilderError.InvalidData) 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 974c88825d..0454b719e3 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 @@ -8,9 +8,10 @@ package io.element.android.libraries.matrix.impl.permalink import androidx.core.net.toUri -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 import io.element.android.libraries.matrix.api.core.RoomId @@ -22,7 +23,6 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser import kotlinx.collections.immutable.toImmutableList import org.matrix.rustcomponents.sdk.MatrixId import org.matrix.rustcomponents.sdk.parseMatrixEntityFrom -import javax.inject.Inject /** * This class turns a uri to a [PermalinkData]. @@ -32,7 +32,8 @@ import javax.inject.Inject * or matrix: permalinks (e.g. matrix:u/chagai95:matrix.org) */ @ContributesBinding(AppScope::class) -class DefaultPermalinkParser @Inject constructor( +@Inject +class DefaultPermalinkParser( private val matrixToConverter: MatrixToConverter ) : PermalinkParser { /** 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 index 1324ebe796..492d5e5792 100644 --- 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 @@ -7,16 +7,17 @@ package io.element.android.libraries.matrix.impl.platform -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 { +@Inject +class RustInitPlatformService : InitPlatformService { override fun init(tracingConfiguration: TracingConfiguration) { initPlatform( config = tracingConfiguration.map(), 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 08809be5f7..e5318671f5 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 @@ -11,11 +11,11 @@ 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 +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext import timber.log.Timber -import javax.inject.Inject /** * Provides the proxy settings from the system. @@ -29,7 +29,8 @@ import javax.inject.Inject * ``` */ @ContributesBinding(AppScope::class) -class DefaultProxyProvider @Inject constructor( +@Inject +class DefaultProxyProvider( @ApplicationContext private val context: Context ) : ProxyProvider { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt index c476c34069..baa9f85906 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt @@ -156,7 +156,7 @@ class JoinedRustRoom( override suspend fun createTimeline( createTimelineParams: CreateTimelineParams, ): Result = withContext(roomDispatcher) { - val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.HideThreadedEvents) + val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.Threads) val focus = when (createTimelineParams) { is CreateTimelineParams.PinnedOnly -> TimelineFocus.PinnedEvents( maxEventsToLoad = 100u, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventType.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventType.kt index 0f7faf0317..14bd6d5e9d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventType.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventType.kt @@ -11,53 +11,55 @@ import io.element.android.libraries.matrix.api.room.MessageEventType import org.matrix.rustcomponents.sdk.MessageLikeEventType fun MessageEventType.map(): MessageLikeEventType = when (this) { - MessageEventType.CALL_ANSWER -> MessageLikeEventType.CALL_ANSWER - MessageEventType.CALL_INVITE -> MessageLikeEventType.CALL_INVITE - MessageEventType.CALL_HANGUP -> MessageLikeEventType.CALL_HANGUP - MessageEventType.CALL_CANDIDATES -> MessageLikeEventType.CALL_CANDIDATES - MessageEventType.CALL_NOTIFY -> MessageLikeEventType.CALL_NOTIFY - MessageEventType.KEY_VERIFICATION_READY -> MessageLikeEventType.KEY_VERIFICATION_READY - MessageEventType.KEY_VERIFICATION_START -> MessageLikeEventType.KEY_VERIFICATION_START - MessageEventType.KEY_VERIFICATION_CANCEL -> MessageLikeEventType.KEY_VERIFICATION_CANCEL - MessageEventType.KEY_VERIFICATION_ACCEPT -> MessageLikeEventType.KEY_VERIFICATION_ACCEPT - MessageEventType.KEY_VERIFICATION_KEY -> MessageLikeEventType.KEY_VERIFICATION_KEY - MessageEventType.KEY_VERIFICATION_MAC -> MessageLikeEventType.KEY_VERIFICATION_MAC - MessageEventType.KEY_VERIFICATION_DONE -> MessageLikeEventType.KEY_VERIFICATION_DONE - MessageEventType.REACTION -> MessageLikeEventType.REACTION - MessageEventType.ROOM_ENCRYPTED -> MessageLikeEventType.ROOM_ENCRYPTED - MessageEventType.ROOM_MESSAGE -> MessageLikeEventType.ROOM_MESSAGE - MessageEventType.ROOM_REDACTION -> MessageLikeEventType.ROOM_REDACTION - MessageEventType.STICKER -> MessageLikeEventType.STICKER - MessageEventType.POLL_END -> MessageLikeEventType.POLL_END - MessageEventType.POLL_RESPONSE -> MessageLikeEventType.POLL_RESPONSE - MessageEventType.POLL_START -> MessageLikeEventType.POLL_START - MessageEventType.UNSTABLE_POLL_END -> MessageLikeEventType.UNSTABLE_POLL_END - MessageEventType.UNSTABLE_POLL_RESPONSE -> MessageLikeEventType.UNSTABLE_POLL_RESPONSE - MessageEventType.UNSTABLE_POLL_START -> MessageLikeEventType.UNSTABLE_POLL_START + MessageEventType.CallAnswer -> MessageLikeEventType.CallAnswer + MessageEventType.CallInvite -> MessageLikeEventType.CallInvite + MessageEventType.CallHangup -> MessageLikeEventType.CallHangup + MessageEventType.CallCandidates -> MessageLikeEventType.CallCandidates + MessageEventType.RtcNotification -> MessageLikeEventType.RtcNotification + MessageEventType.KeyVerificationReady -> MessageLikeEventType.KeyVerificationReady + MessageEventType.KeyVerificationStart -> MessageLikeEventType.KeyVerificationStart + MessageEventType.KeyVerificationCancel -> MessageLikeEventType.KeyVerificationCancel + MessageEventType.KeyVerificationAccept -> MessageLikeEventType.KeyVerificationAccept + MessageEventType.KeyVerificationKey -> MessageLikeEventType.KeyVerificationKey + MessageEventType.KeyVerificationMac -> MessageLikeEventType.KeyVerificationMac + MessageEventType.KeyVerificationDone -> MessageLikeEventType.KeyVerificationDone + MessageEventType.Reaction -> MessageLikeEventType.Reaction + MessageEventType.RoomEncrypted -> MessageLikeEventType.RoomEncrypted + MessageEventType.RoomMessage -> MessageLikeEventType.RoomMessage + MessageEventType.RoomRedaction -> MessageLikeEventType.RoomRedaction + MessageEventType.Sticker -> MessageLikeEventType.Sticker + MessageEventType.PollEnd -> MessageLikeEventType.PollEnd + MessageEventType.PollResponse -> MessageLikeEventType.PollResponse + MessageEventType.PollStart -> MessageLikeEventType.PollStart + MessageEventType.UnstablePollEnd -> MessageLikeEventType.UnstablePollEnd + MessageEventType.UnstablePollResponse -> MessageLikeEventType.UnstablePollResponse + MessageEventType.UnstablePollStart -> MessageLikeEventType.UnstablePollStart + is MessageEventType.Other -> MessageLikeEventType.Other(type) } fun MessageLikeEventType.map(): MessageEventType = when (this) { - MessageLikeEventType.CALL_ANSWER -> MessageEventType.CALL_ANSWER - MessageLikeEventType.CALL_INVITE -> MessageEventType.CALL_INVITE - MessageLikeEventType.CALL_HANGUP -> MessageEventType.CALL_HANGUP - MessageLikeEventType.CALL_CANDIDATES -> MessageEventType.CALL_CANDIDATES - MessageLikeEventType.CALL_NOTIFY -> MessageEventType.CALL_NOTIFY - MessageLikeEventType.KEY_VERIFICATION_READY -> MessageEventType.KEY_VERIFICATION_READY - MessageLikeEventType.KEY_VERIFICATION_START -> MessageEventType.KEY_VERIFICATION_START - MessageLikeEventType.KEY_VERIFICATION_CANCEL -> MessageEventType.KEY_VERIFICATION_CANCEL - MessageLikeEventType.KEY_VERIFICATION_ACCEPT -> MessageEventType.KEY_VERIFICATION_ACCEPT - MessageLikeEventType.KEY_VERIFICATION_KEY -> MessageEventType.KEY_VERIFICATION_KEY - MessageLikeEventType.KEY_VERIFICATION_MAC -> MessageEventType.KEY_VERIFICATION_MAC - MessageLikeEventType.KEY_VERIFICATION_DONE -> MessageEventType.KEY_VERIFICATION_DONE - MessageLikeEventType.REACTION -> MessageEventType.REACTION - MessageLikeEventType.ROOM_ENCRYPTED -> MessageEventType.ROOM_ENCRYPTED - MessageLikeEventType.ROOM_MESSAGE -> MessageEventType.ROOM_MESSAGE - MessageLikeEventType.ROOM_REDACTION -> MessageEventType.ROOM_REDACTION - MessageLikeEventType.STICKER -> MessageEventType.STICKER - MessageLikeEventType.POLL_END -> MessageEventType.POLL_END - MessageLikeEventType.POLL_RESPONSE -> MessageEventType.POLL_RESPONSE - MessageLikeEventType.POLL_START -> MessageEventType.POLL_START - MessageLikeEventType.UNSTABLE_POLL_END -> MessageEventType.UNSTABLE_POLL_END - MessageLikeEventType.UNSTABLE_POLL_RESPONSE -> MessageEventType.UNSTABLE_POLL_RESPONSE - MessageLikeEventType.UNSTABLE_POLL_START -> MessageEventType.UNSTABLE_POLL_START + MessageLikeEventType.CallAnswer -> MessageEventType.CallAnswer + MessageLikeEventType.CallInvite -> MessageEventType.CallInvite + MessageLikeEventType.CallHangup -> MessageEventType.CallHangup + MessageLikeEventType.CallCandidates -> MessageEventType.CallCandidates + MessageLikeEventType.RtcNotification -> MessageEventType.RtcNotification + MessageLikeEventType.KeyVerificationReady -> MessageEventType.KeyVerificationReady + MessageLikeEventType.KeyVerificationStart -> MessageEventType.KeyVerificationStart + MessageLikeEventType.KeyVerificationCancel -> MessageEventType.KeyVerificationCancel + MessageLikeEventType.KeyVerificationAccept -> MessageEventType.KeyVerificationAccept + MessageLikeEventType.KeyVerificationKey -> MessageEventType.KeyVerificationKey + MessageLikeEventType.KeyVerificationMac -> MessageEventType.KeyVerificationMac + MessageLikeEventType.KeyVerificationDone -> MessageEventType.KeyVerificationDone + MessageLikeEventType.Reaction -> MessageEventType.Reaction + MessageLikeEventType.RoomEncrypted -> MessageEventType.RoomEncrypted + MessageLikeEventType.RoomMessage -> MessageEventType.RoomMessage + MessageLikeEventType.RoomRedaction -> MessageEventType.RoomRedaction + MessageLikeEventType.Sticker -> MessageEventType.Sticker + MessageLikeEventType.PollEnd -> MessageEventType.PollEnd + MessageLikeEventType.PollResponse -> MessageEventType.PollResponse + MessageLikeEventType.PollStart -> MessageEventType.PollStart + MessageLikeEventType.UnstablePollEnd -> MessageEventType.UnstablePollEnd + MessageLikeEventType.UnstablePollResponse -> MessageEventType.UnstablePollResponse + MessageLikeEventType.UnstablePollStart -> MessageEventType.UnstablePollStart + is MessageLikeEventType.Other -> MessageEventType.Other(v1) } 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 9d7495691d..323b4b50f8 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 @@ -14,7 +14,6 @@ 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.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 @@ -63,9 +62,6 @@ class RoomContentForwarder( } }.onFailure { failedForwardingTo.add(RoomId(room.id())) - if (it is CancellationException) { - throw it - } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt index 975185242b..6061ebd79c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt @@ -38,10 +38,12 @@ 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.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.CallDeclineListener import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.use import timber.log.Timber @@ -155,7 +157,11 @@ class RustBaseRoom( runCatchingExceptions { innerRoom.leave() }.onSuccess { - roomMembershipObserver.notifyUserLeftRoom(roomId, membershipBeforeLeft) + roomMembershipObserver.notifyUserLeftRoom( + roomId = roomId, + isSpace = roomInfoFlow.value.isSpace, + membershipBeforeLeft = membershipBeforeLeft, + ) } } @@ -300,4 +306,28 @@ class RustBaseRoom( innerRoom.reportRoom(reason.orEmpty()) } } + + override suspend fun declineCall(notificationEventId: EventId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.declineCall(notificationEventId.value) + } + } + + override suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow = withContext(roomDispatcher) { + mxCallbackFlow { + innerRoom.subscribeToCallDeclineEvents(notificationEventId.value, object : CallDeclineListener { + override fun call(declinerUserId: String) { + trySend(UserId(declinerUserId)) + } + }) + } + } + + override suspend fun threadRootIdForEvent(eventId: EventId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.loadOrFetchEvent(eventId.value).use { + it.threadRootEventId()?.let(::ThreadId) + } + } + } } 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 db681416de..ee9e7bfe52 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 @@ -108,7 +108,7 @@ class RustRoomFactory( val sdkRoom = awaitRoomInRoomList(roomId) ?: return@withContext null if (sdkRoom.membership() == Membership.JOINED) { - val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.HideThreadedEvents) + val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.Threads) // Init the live timeline in the SDK from the Room val timeline = sdkRoom.timelineWithConfiguration( TimelineConfiguration( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/TimelineEventTypeFilterFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/TimelineEventTypeFilterFactory.kt index 126c288a9d..29f72848b8 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/TimelineEventTypeFilterFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/TimelineEventTypeFilterFactory.kt @@ -7,19 +7,20 @@ package io.element.android.libraries.matrix.impl.room -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.room.StateEventType import org.matrix.rustcomponents.sdk.FilterTimelineEventType import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter -import javax.inject.Inject interface TimelineEventTypeFilterFactory { fun create(listStateEventType: List): TimelineEventTypeFilter } @ContributesBinding(AppScope::class) -class RustTimelineEventTypeFilterFactory @Inject constructor() : TimelineEventTypeFilterFactory { +@Inject +class RustTimelineEventTypeFilterFactory : TimelineEventTypeFilterFactory { override fun create(listStateEventType: List): TimelineEventTypeFilter { return TimelineEventTypeFilter.exclude( listStateEventType.map { stateEventType -> diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/alias/DefaultRoomAliasHelper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/alias/DefaultRoomAliasHelper.kt index a8d9abfc80..039144bf55 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/alias/DefaultRoomAliasHelper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/alias/DefaultRoomAliasHelper.kt @@ -7,14 +7,15 @@ package io.element.android.libraries.matrix.impl.room.alias -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultRoomAliasHelper @Inject constructor() : RoomAliasHelper { +@Inject +class DefaultRoomAliasHelper : RoomAliasHelper { override fun roomAliasNameFromRoomDisplayName(name: String): String { return org.matrix.rustcomponents.sdk.roomAliasNameFromRoomDisplayName(name) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt index ae74e1edc0..a93ce58236 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt @@ -20,7 +20,7 @@ fun RustAllowRule.map(): AllowRule { fun AllowRule.map(): RustAllowRule { return when (this) { - is AllowRule.RoomMembership -> RustAllowRule.RoomMembership(roomId.toString()) + is AllowRule.RoomMembership -> RustAllowRule.RoomMembership(roomId.value) is AllowRule.Custom -> RustAllowRule.Custom(json) } } 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 98a2175df9..5a790acd7a 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 @@ -7,7 +7,8 @@ package io.element.android.libraries.matrix.impl.room.join -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.di.SessionScope @@ -18,10 +19,10 @@ 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.impl.analytics.toAnalyticsJoinedRoom import io.element.android.services.analytics.api.AnalyticsService -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultJoinRoom @Inject constructor( +@Inject +class DefaultJoinRoom( private val client: MatrixClient, private val analyticsService: AnalyticsService, ) : JoinRoom { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt index bc10a369a4..51422787e2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.room.join import io.element.android.libraries.matrix.api.room.join.JoinRule +import kotlinx.collections.immutable.toPersistentList import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule fun RustJoinRule.map(): JoinRule { @@ -16,9 +17,9 @@ fun RustJoinRule.map(): JoinRule { RustJoinRule.Private -> JoinRule.Private RustJoinRule.Knock -> JoinRule.Knock RustJoinRule.Invite -> JoinRule.Invite - is RustJoinRule.Restricted -> JoinRule.Restricted(rules.map { it.map() }) + is RustJoinRule.Restricted -> JoinRule.Restricted(rules.map { it.map() }.toPersistentList()) is RustJoinRule.Custom -> JoinRule.Custom(repr) - is RustJoinRule.KnockRestricted -> JoinRule.KnockRestricted(rules.map { it.map() }) + is RustJoinRule.KnockRestricted -> JoinRule.KnockRestricted(rules.map { it.map() }.toPersistentList()) } } 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 187bead11e..fc394e4ce7 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 @@ -79,35 +79,35 @@ internal class RoomMemberListFetcher( 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()) + value = pendingWithCurrentMembers() val members = parseAndEmitMembers(room.membersNoSync()) val newState = if (asPendingState) { RoomMembersState.Pending(prevRoomMembers = members) } else { RoomMembersState.Ready(members) } - emit(newState) + value = newState } catch (exception: CancellationException) { Timber.d("Cancelled loading cached members for room $roomId") throw exception } catch (exception: Exception) { Timber.e(exception, "Failed to load cached members for room $roomId") - emit(RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList())) + value = RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList()) } } 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()) + value = pendingWithCurrentMembers() // Start loading new members - emit(RoomMembersState.Ready(parseAndEmitMembers(room.members()))) + value = 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(RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList())) + value = RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.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 9411ae3aab..af7376e445 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,7 +25,6 @@ object RoomMemberMapper { membership = mapMembership(roomMember.membership), isNameAmbiguous = roomMember.isNameAmbiguous, powerLevel = powerLevel, - normalizedPowerLevel = roomMember.normalizedPowerLevel.into(), isIgnored = roomMember.isIgnored, role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel), membershipChangeReason = roomMember.membershipChangeReason 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 66931a1f13..e26cea1c94 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 @@ -33,6 +33,12 @@ class RoomSummaryListProcessor( updates.forEach { update -> applyUpdate(update) } + + // TODO remove once https://github.com/element-hq/element-x-android/issues/5031 has been confirmed as fixed + val duplicates = groupingBy { it.roomId }.eachCount().filter { it.value > 1 } + if (duplicates.isNotEmpty()) { + Timber.e("Found duplicates in room summaries after a list update from the SDK: $duplicates. Updates: $updates") + } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/server/DefaultUserServerResolver.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/server/DefaultUserServerResolver.kt index 584f7931fe..ff62708f65 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/server/DefaultUserServerResolver.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/server/DefaultUserServerResolver.kt @@ -7,14 +7,15 @@ package io.element.android.libraries.matrix.impl.server -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.server.UserServerResolver -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultUserServerResolver @Inject constructor( +@Inject +class DefaultUserServerResolver( private val matrixClient: MatrixClient, ) : UserServerResolver { override fun resolve(): String { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceRoomList.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceRoomList.kt new file mode 100644 index 0000000000..b94c3ffd1b --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceRoomList.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.spaces + +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import timber.log.Timber +import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState +import java.util.Optional +import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList + +class RustSpaceRoomList( + override val roomId: RoomId, + private val innerProvider: suspend () -> InnerSpaceRoomList, + private val coroutineScope: CoroutineScope, + spaceRoomMapper: SpaceRoomMapper, +) : SpaceRoomList { + private val innerCompletable = CompletableDeferred() + + override val currentSpaceFlow = MutableStateFlow>(Optional.empty()) + + override val spaceRoomsFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = Int.MAX_VALUE) + + override val paginationStatusFlow: MutableStateFlow = + MutableStateFlow(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false)) + private val spaceListUpdateProcessor = SpaceListUpdateProcessor( + spaceRoomsFlow = spaceRoomsFlow, + mapper = spaceRoomMapper + ) + + init { + coroutineScope.launch { + val inner = innerProvider() + innerCompletable.complete(inner) + + inner.paginationStateFlow() + .onEach { paginationStatus -> + paginationStatusFlow.emit(paginationStatus.into()) + } + .launchIn(this) + + inner.spaceListUpdateFlow() + .onEach { updates -> + spaceListUpdateProcessor.postUpdates(updates) + } + .launchIn(this) + + inner.spaceUpdateFlow() + .map { space -> space.map(spaceRoomMapper::map) } + .onEach { space -> + currentSpaceFlow.emit(space) + } + .launchIn(this) + } + } + + override suspend fun paginate(): Result { + return runCatchingExceptions { + innerCompletable.await().paginate() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun destroy() { + Timber.d("Destroying SpaceRoomList $roomId") + coroutineScope.cancel() + try { + innerCompletable.getCompleted().destroy() + } catch (_: Exception) { + // Ignore, we just want to make sure it's completed + } + } + + private fun SpaceRoomListPaginationState.into(): SpaceRoomList.PaginationStatus { + return when (this) { + is SpaceRoomListPaginationState.Idle -> SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = !endReached) + SpaceRoomListPaginationState.Loading -> SpaceRoomList.PaginationStatus.Loading + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt new file mode 100644 index 0000000000..86d50a477c --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.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.libraries.matrix.impl.spaces + +import io.element.android.libraries.core.coroutine.childScope +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList +import io.element.android.libraries.matrix.api.spaces.SpaceService +import io.element.android.libraries.matrix.impl.util.cancelAndDestroy +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.SpaceListUpdate +import org.matrix.rustcomponents.sdk.SpaceServiceInterface +import org.matrix.rustcomponents.sdk.SpaceServiceJoinedSpacesListener +import timber.log.Timber +import org.matrix.rustcomponents.sdk.SpaceService as ClientSpaceService + +class RustSpaceService( + private val innerSpaceService: ClientSpaceService, + private val sessionCoroutineScope: CoroutineScope, + private val sessionDispatcher: CoroutineDispatcher, +) : SpaceService { + private val spaceRoomMapper = SpaceRoomMapper() + override val spaceRoomsFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) + private val spaceListUpdateProcessor = SpaceListUpdateProcessor( + spaceRoomsFlow = spaceRoomsFlow, + mapper = spaceRoomMapper + ) + + override suspend fun joinedSpaces(): Result> = withContext(sessionDispatcher) { + runCatchingExceptions { + innerSpaceService.joinedSpaces() + .map { + it.let(spaceRoomMapper::map) + } + } + } + + override fun spaceRoomList(id: RoomId): SpaceRoomList { + val childCoroutineScope = sessionCoroutineScope.childScope(sessionDispatcher, "SpaceRoomListScope-$this") + return RustSpaceRoomList( + roomId = id, + innerProvider = { innerSpaceService.spaceRoomList(id.value) }, + coroutineScope = childCoroutineScope, + spaceRoomMapper = spaceRoomMapper, + ) + } + + init { + innerSpaceService + .spaceListUpdate() + .onEach { updates -> + spaceListUpdateProcessor.postUpdates(updates) + } + .launchIn(sessionCoroutineScope) + } +} + +internal fun SpaceServiceInterface.spaceListUpdate(): Flow> = + callbackFlow { + val listener = object : SpaceServiceJoinedSpacesListener { + override fun onUpdate(roomUpdates: List) { + trySendBlocking(roomUpdates) + } + } + Timber.d("Open spaceDiffFlow for SpaceServiceInterface ${this@spaceListUpdate}") + val taskHandle = subscribeToJoinedSpaces(listener) + awaitClose { + Timber.d("Close spaceDiffFlow for SpaceServiceInterface ${this@spaceListUpdate}") + taskHandle.cancelAndDestroy() + } + }.catch { + Timber.d(it, "spaceDiffFlow() failed") + }.buffer(Channel.UNLIMITED) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceListUpdateProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceListUpdateProcessor.kt new file mode 100644 index 0000000000..40310561e2 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceListUpdateProcessor.kt @@ -0,0 +1,84 @@ +/* + * 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.spaces + +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.matrix.rustcomponents.sdk.SpaceListUpdate +import timber.log.Timber + +internal class SpaceListUpdateProcessor( + private val spaceRoomsFlow: MutableSharedFlow>, + private val mapper: SpaceRoomMapper, +) { + private val mutex = Mutex() + + suspend fun postUpdates(updates: List) { + Timber.v("Update space rooms from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}") + updateSpaceRooms { + updates.forEach { update -> applyUpdate(update) } + } + } + + private suspend fun updateSpaceRooms(block: MutableList.() -> Unit) = + mutex.withLock { + val spaceRooms = if (spaceRoomsFlow.replayCache.isNotEmpty()) { + spaceRoomsFlow.first().toMutableList() + } else { + mutableListOf() + } + block(spaceRooms) + spaceRoomsFlow.emit(spaceRooms) + } + + private fun MutableList.applyUpdate(update: SpaceListUpdate) { + when (update) { + is SpaceListUpdate.Append -> { + val newSpaces = update.values.map(mapper::map) + addAll(newSpaces) + } + SpaceListUpdate.Clear -> clear() + is SpaceListUpdate.Insert -> { + val newSpace = mapper.map(update.value) + add(update.index.toInt(), newSpace) + } + SpaceListUpdate.PopBack -> { + removeAt(lastIndex) + } + SpaceListUpdate.PopFront -> { + removeAt(0) + } + is SpaceListUpdate.PushBack -> { + val newSpace = mapper.map(update.value) + add(newSpace) + } + is SpaceListUpdate.PushFront -> { + val newSpace = mapper.map(update.value) + add(0, newSpace) + } + is SpaceListUpdate.Remove -> { + removeAt(update.index.toInt()) + } + is SpaceListUpdate.Reset -> { + clear() + val newSpaces = update.values.map(mapper::map) + addAll(newSpaces) + } + is SpaceListUpdate.Set -> { + val newSpace = mapper.map(update.value) + this[update.index.toInt()] = newSpace + } + is SpaceListUpdate.Truncate -> { + subList(update.length.toInt(), size).clear() + } + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomListExtensions.kt new file mode 100644 index 0000000000..c7107f745f --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomListExtensions.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.spaces + +import io.element.android.libraries.matrix.impl.util.cancelAndDestroy +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import org.matrix.rustcomponents.sdk.SpaceListUpdate +import org.matrix.rustcomponents.sdk.SpaceRoom +import org.matrix.rustcomponents.sdk.SpaceRoomListEntriesListener +import org.matrix.rustcomponents.sdk.SpaceRoomListInterface +import org.matrix.rustcomponents.sdk.SpaceRoomListPaginationStateListener +import org.matrix.rustcomponents.sdk.SpaceRoomListSpaceListener +import timber.log.Timber +import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState +import java.util.Optional + +internal fun SpaceRoomListInterface.paginationStateFlow(): Flow = callbackFlow { + val listener = object : SpaceRoomListPaginationStateListener { + override fun onUpdate(paginationState: SpaceRoomListPaginationState) { + trySend(paginationState) + } + } + // Send the initial value + trySend(paginationState()) + // Then subscribe to updates + val result = subscribeToPaginationStateUpdates(listener) + awaitClose { + result.cancelAndDestroy() + } +}.catch { + Timber.d(it, "paginationStateFlow() failed") +}.buffer(Channel.UNLIMITED) + +internal fun SpaceRoomListInterface.spaceListUpdateFlow(): Flow> = + callbackFlow { + val listener = object : SpaceRoomListEntriesListener { + override fun onUpdate(rooms: List) { + trySendBlocking(rooms) + } + } + Timber.d("Open spaceListUpdateFlow for SpaceRoomListInterface ${this@spaceListUpdateFlow}") + val taskHandle = subscribeToRoomUpdate(listener) + awaitClose { + Timber.d("Close spaceListUpdateFlow for SpaceRoomListInterface ${this@spaceListUpdateFlow}") + taskHandle.cancelAndDestroy() + } + }.catch { + Timber.d(it, "spaceListUpdateFlow() failed") + }.buffer(Channel.UNLIMITED) + +internal fun SpaceRoomListInterface.spaceUpdateFlow(): Flow> = + callbackFlow { + val listener = object : SpaceRoomListSpaceListener { + override fun onUpdate(space: SpaceRoom?) { + trySendBlocking(Optional.ofNullable(space)) + } + } + Timber.d("Open spaceUpdateFlow for SpaceRoomListInterface ${this@spaceUpdateFlow}") + trySendBlocking(Optional.ofNullable(space())) + val taskHandle = subscribeToSpaceUpdates(listener) + awaitClose { + Timber.d("Close spaceUpdateFlow for SpaceRoomListInterface ${this@spaceUpdateFlow}") + taskHandle.cancelAndDestroy() + } + }.catch { + Timber.d(it, "spaceUpdateFlow() failed") + }.buffer(Channel.UNLIMITED) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomMapper.kt new file mode 100644 index 0000000000..7032d052d8 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomMapper.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.matrix.impl.spaces + +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.spaces.SpaceRoom +import io.element.android.libraries.matrix.impl.room.join.map +import io.element.android.libraries.matrix.impl.room.map +import org.matrix.rustcomponents.sdk.SpaceRoom as RustSpaceRoom + +class SpaceRoomMapper { + fun map(spaceRoom: RustSpaceRoom): SpaceRoom { + return SpaceRoom( + avatarUrl = spaceRoom.avatarUrl, + canonicalAlias = spaceRoom.canonicalAlias?.let(::RoomAlias), + childrenCount = spaceRoom.childrenCount.toInt(), + guestCanJoin = spaceRoom.guestCanJoin, + heroes = spaceRoom.heroes.orEmpty().map { it.map() }, + joinRule = spaceRoom.joinRule?.map(), + name = spaceRoom.name, + numJoinedMembers = spaceRoom.numJoinedMembers.toInt(), + roomId = RoomId(spaceRoom.roomId), + roomType = spaceRoom.roomType.map(), + state = spaceRoom.state?.map(), + topic = spaceRoom.topic, + worldReadable = spaceRoom.worldReadable.orFalse(), + via = spaceRoom.via, + ) + } +} 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 ee60a464e7..4c52e78205 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.androidutils.hash.hash import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -122,7 +123,7 @@ class RustTimeline( ) override val forwardPaginationStatus = MutableStateFlow( - Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode !is Timeline.Mode.FocusedOnEvent) + Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode is Timeline.Mode.FocusedOnEvent) ) init { @@ -220,7 +221,6 @@ class RustTimeline( items = items, hasMoreToLoadBackward = backwardPaginationStatus.hasMoreToLoad, hasMoreToLoadForward = forwardPaginationStatus.hasMoreToLoad, - timelineMode = mode, ) } .let { items -> @@ -338,6 +338,7 @@ class RustTimeline( formattedCaption: String?, inReplyToEventId: EventId?, ): Result { + Timber.d("Sending image ${file.path.hash()}") return sendAttachment(listOfNotNull(file, thumbnailFile)) { inner.sendImage( params = UploadParameters( @@ -363,6 +364,7 @@ class RustTimeline( formattedCaption: String?, inReplyToEventId: EventId?, ): Result { + Timber.d("Sending video ${file.path.hash()}") return sendAttachment(listOfNotNull(file, thumbnailFile)) { inner.sendVideo( params = UploadParameters( @@ -387,6 +389,7 @@ class RustTimeline( formattedCaption: String?, inReplyToEventId: EventId?, ): Result { + Timber.d("Sending audio ${file.path.hash()}") return sendAttachment(listOf(file)) { inner.sendAudio( params = UploadParameters( @@ -410,6 +413,7 @@ class RustTimeline( formattedCaption: String?, inReplyToEventId: EventId?, ): Result { + Timber.d("Sending file ${file.path.hash()}") return sendAttachment(listOf(file)) { inner.sendFile( params = UploadParameters( @@ -426,7 +430,7 @@ class RustTimeline( } } - override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result = withContext(dispatcher) { + override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result = withContext(dispatcher) { runCatchingExceptions { inner.toggleReaction( key = emoji, 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 aa20ffd267..ed38c328f6 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 @@ -38,7 +38,7 @@ private const val MSG_TYPE_GALLERY_UNSTABLE = "dm.filament.gallery" class EventMessageMapper { private val inReplyToMapper by lazy { InReplyToMapper(TimelineEventContentMapper()) } - fun map(message: MsgLikeKind.Message, inReplyTo: InReplyToDetails?, threadInfo: EventThreadInfo): MessageContent = message.use { + fun map(message: MsgLikeKind.Message, inReplyTo: InReplyToDetails?, threadInfo: EventThreadInfo?): MessageContent = message.use { val type = it.content.msgType.use(this::mapMessageType) val inReplyToEvent: InReplyTo? = inReplyTo?.use(inReplyToMapper::map) MessageContent( 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 eeb063b28a..1b941009e0 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 @@ -27,6 +27,7 @@ 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 @@ -79,7 +80,7 @@ class TimelineEventContentMapper( content = map(latestEvent.content), senderId = UserId(latestEvent.sender), senderProfile = latestEvent.senderProfile.map(), - timestamp = latestEvent.timestamp.toLong() + timestamp = latestEvent.timestamp.toLong(), ) ) } @@ -89,10 +90,12 @@ class TimelineEventContentMapper( numberOfReplies = numberOfReplies, ) } - val threadInfo = EventThreadInfo( - threadRootId = it.content.threadRoot?.let(::ThreadId), - threadSummary = threadSummary, - ) + val threadRootId = it.content.threadRoot?.let(::ThreadId) + val threadInfo = when { + threadSummary != null -> EventThreadInfo.ThreadRoot(threadSummary) + threadRootId != null -> EventThreadInfo.ThreadResponse(threadRootId) + else -> null + } eventMessageMapper.map(kind, inReplyTo, threadInfo) } is MsgLikeKind.Redacted -> { @@ -124,6 +127,7 @@ class TimelineEventContentMapper( source = kind.source.map(), ) } + is MsgLikeKind.Other -> UnknownContent } } is TimelineItemContent.ProfileChange -> { @@ -149,7 +153,7 @@ class TimelineEventContentMapper( ) } is TimelineItemContent.CallInvite -> LegacyCallInviteContent - is TimelineItemContent.CallNotify -> CallNotifyContent + is TimelineItemContent.RtcNotification -> CallNotifyContent } } } 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 22b70b1d35..6d42af54b5 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 @@ -18,9 +18,8 @@ class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) { items: List, hasMoreToLoadBackward: Boolean, hasMoreToLoadForward: Boolean, - timelineMode: Timeline.Mode, ): List { - val shouldAddForwardLoadingIndicator = timelineMode is Timeline.Mode.Live && hasMoreToLoadForward && items.isNotEmpty() + val shouldAddForwardLoadingIndicator = hasMoreToLoadForward && items.isNotEmpty() val currentTimestamp = systemClock.epochMillis() return buildList { if (hasMoreToLoadBackward) { 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 728bf136b8..9df7f459ac 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 @@ -7,9 +7,10 @@ package io.element.android.libraries.matrix.impl.tracing -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.tracing.LogLevel import io.element.android.libraries.matrix.api.tracing.TracingConfiguration import io.element.android.libraries.matrix.api.tracing.TracingService @@ -17,10 +18,10 @@ import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration import org.matrix.rustcomponents.sdk.TracingFileConfiguration import org.matrix.rustcomponents.sdk.reloadTracingFileWriter import timber.log.Timber -import javax.inject.Inject @ContributesBinding(AppScope::class) -class RustTracingService @Inject constructor(private val buildMeta: BuildMeta) : TracingService { +@Inject +class RustTracingService(private val buildMeta: BuildMeta) : TracingService { override fun createTimberTree(target: String): Timber.Tree { return RustTracingTree(target = target, retrieveFromStackTrace = buildMeta.isDebuggable) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserSearchResultMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserSearchResultMapper.kt index 5b2e8fa9a2..172f3c4cb0 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserSearchResultMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserSearchResultMapper.kt @@ -8,13 +8,16 @@ package io.element.android.libraries.matrix.impl.usersearch import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults +import io.element.android.libraries.matrix.impl.mapper.map import kotlinx.collections.immutable.toImmutableList import org.matrix.rustcomponents.sdk.SearchUsersResults object UserSearchResultMapper { fun map(result: SearchUsersResults): MatrixSearchUserResults { return MatrixSearchUserResults( - results = result.results.map(UserProfileMapper::map).toImmutableList(), + results = result.results + .map { userProfile -> userProfile.map() } + .toImmutableList(), limited = result.limited, ) } 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 d2e15a3fac..cd398fbfc1 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 @@ -12,22 +12,17 @@ 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 io.element.android.libraries.matrix.impl.mapper.map import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails -import org.matrix.rustcomponents.sdk.UserProfile as RustUserProfile fun RustSessionVerificationRequestDetails.map() = SessionVerificationRequestDetails( senderProfile = senderProfile.map(), flowId = FlowId(flowId), deviceId = DeviceId(deviceId), + deviceDisplayName = deviceDisplayName, 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) { 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 863dd6f0a2..00947f99a2 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 @@ -7,42 +7,38 @@ package io.element.android.libraries.matrix.impl.widget -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.flow.first import org.matrix.rustcomponents.sdk.newVirtualElementCallWidget +import timber.log.Timber import uniffi.matrix_sdk.EncryptionSystem -import uniffi.matrix_sdk.HeaderStyle -import uniffi.matrix_sdk.NotificationType -import uniffi.matrix_sdk.VirtualElementCallWidgetOptions -import javax.inject.Inject +import uniffi.matrix_sdk.VirtualElementCallWidgetConfig +import uniffi.matrix_sdk.VirtualElementCallWidgetProperties import uniffi.matrix_sdk.Intent as CallIntent @ContributesBinding(AppScope::class) -class DefaultCallWidgetSettingsProvider @Inject constructor( +@Inject +class DefaultCallWidgetSettingsProvider( private val buildMeta: BuildMeta, private val callAnalyticsCredentialsProvider: CallAnalyticCredentialsProvider, private val analyticsService: AnalyticsService, ) : CallWidgetSettingsProvider { - override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean, direct: Boolean): MatrixWidgetSettings { + override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean, direct: Boolean, hasActiveCall: Boolean): MatrixWidgetSettings { val isAnalyticsEnabled = analyticsService.userConsentFlow.first() - val options = VirtualElementCallWidgetOptions( + val properties = VirtualElementCallWidgetProperties( elementCallUrl = baseUrl, widgetId = widgetId, - preload = null, fontScale = null, - appPrompt = false, - confineToRoom = true, font = 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 }, @@ -50,13 +46,25 @@ class DefaultCallWidgetSettingsProvider @Inject constructor( 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, - sendNotificationType = if (direct) NotificationType.RING else NotificationType.NOTIFICATION, ) - val rustWidgetSettings = newVirtualElementCallWidget(options) + val config = VirtualElementCallWidgetConfig( + // TODO remove this once we have the next EC version + preload = false, + // TODO remove this once we have the next EC version + skipLobby = null, + intent = when { + direct && hasActiveCall -> CallIntent.JOIN_EXISTING_DM + hasActiveCall -> CallIntent.JOIN_EXISTING + direct -> CallIntent.START_CALL_DM + else -> CallIntent.START_CALL + }.also { + Timber.d("Starting/joining call with intent: $it") + } + ) + val rustWidgetSettings = newVirtualElementCallWidget( + props = properties, + config = config, + ) 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 3406b6686a..89e0574fdf 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 @@ -10,8 +10,10 @@ package io.element.android.libraries.matrix.impl import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder import org.matrix.rustcomponents.sdk.ClientBuilder -class FakeClientBuilderProvider : ClientBuilderProvider { +class FakeClientBuilderProvider( + private val provideResult: () -> ClientBuilder = { FakeFfiClientBuilder() } +) : ClientBuilderProvider { override fun provide(): ClientBuilder { - return FakeFfiClientBuilder() + return provideResult() } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegateTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegateTest.kt index d8b0baa67a..224c4118fb 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegateTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegateTest.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.impl import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession import io.element.android.libraries.sessionstorage.api.SessionStore -import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -23,11 +23,12 @@ import org.junit.Test class RustClientSessionDelegateTest { @Test fun `saveSessionInKeychain should update the store`() = runTest { - val sessionStore = InMemorySessionStore() - sessionStore.storeData( - aSessionData( - accessToken = "anAccessToken", - refreshToken = "aRefreshToken", + val sessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData( + accessToken = "anAccessToken", + refreshToken = "aRefreshToken", + ) ) ) val sut = aRustClientSessionDelegate(sessionStore) 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 c051aebbac..045b5d7772 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 @@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.impl.auth.FakeUserCertificatesProvide import io.element.android.libraries.matrix.impl.room.FakeTimelineEventTypeFilterFactory import io.element.android.libraries.network.useragent.SimpleUserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionStore -import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.toolbox.test.systemclock.FakeSystemClock @@ -38,7 +38,10 @@ class RustMatrixClientFactoryTest { fun TestScope.createRustMatrixClientFactory( baseDirectory: File = File("/base"), cacheDirectory: File = File("/cache"), - sessionStore: SessionStore = InMemorySessionStore(), + sessionStore: SessionStore = InMemorySessionStore( + updateUserProfileResult = { _, _, _ -> }, + ), + clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(), ) = RustMatrixClientFactory( baseDirectory = baseDirectory, cacheDirectory = cacheDirectory, @@ -52,5 +55,5 @@ fun TestScope.createRustMatrixClientFactory( analyticsService = FakeAnalyticsService(), featureFlagService = FakeFeatureFlagService(), timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(), - clientBuilderProvider = FakeClientBuilderProvider(), + clientBuilderProvider = clientBuilderProvider, ) 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 91d0f06d1c..6eed2d0967 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 @@ -5,6 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.libraries.matrix.impl import com.google.common.truth.Truth.assertThat @@ -12,17 +14,24 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService 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.AN_AVATAR_URL 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.matrix.test.A_USER_NAME import io.element.android.libraries.sessionstorage.api.SessionStore -import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.UserProfile import java.io.File class RustMatrixClientTest { @@ -51,9 +60,46 @@ class RustMatrixClientTest { client.destroy() } + @Test + fun `retrieving the UserProfile updates the database`() = runTest { + val updateUserProfileResult = lambdaRecorder { _, _, _ -> } + val sessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData( + sessionId = A_USER_ID.value, + userDisplayName = null, + userAvatarUrl = null, + ) + ), + updateUserProfileResult = updateUserProfileResult, + ) + val client = createRustMatrixClient( + client = FakeFfiClient( + getProfileResult = { userId -> + UserProfile( + userId = userId, + displayName = A_USER_NAME, + avatarUrl = AN_AVATAR_URL, + ) + }, + ), + sessionStore = sessionStore, + ) + advanceUntilIdle() + updateUserProfileResult.assertions().isCalledOnce() + .with( + value(A_USER_ID.value), + value(A_USER_NAME), + value(AN_AVATAR_URL), + ) + client.destroy() + } + private fun TestScope.createRustMatrixClient( client: Client = FakeFfiClient(), - sessionStore: SessionStore = InMemorySessionStore(), + sessionStore: SessionStore = InMemorySessionStore( + updateUserProfileResult = { _, _, _ -> }, + ), ) = RustMatrixClient( innerClient = client, baseDirectory = File(""), 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 31453a7ca5..1cb5db91f6 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 @@ -8,14 +8,17 @@ package io.element.android.libraries.matrix.impl.auth import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.impl.ClientBuilderProvider +import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverLoginDetails import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider import io.element.android.libraries.matrix.test.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 +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -24,18 +27,28 @@ import java.io.File class RustMatrixAuthenticationServiceTest { @Test - fun `getLatestSessionId should return the value from the store`() = runTest { - val sessionStore = InMemorySessionStore() + fun `setHomeserver is successful`() = runTest { val sut = createRustMatrixAuthenticationService( - sessionStore = sessionStore, + clientBuilderProvider = FakeClientBuilderProvider( + provideResult = { + FakeFfiClientBuilder( + buildResult = { + FakeFfiClient( + homeserverLoginDetailsResult = { + FakeFfiHomeserverLoginDetails() + } + ) + } + ) + } + ), ) - assertThat(sut.getLatestSessionId()).isNull() - sessionStore.storeData(aSessionData(sessionId = "@alice:server.org")) - assertThat(sut.getLatestSessionId()).isEqualTo(SessionId("@alice:server.org")) + assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue() } private fun TestScope.createRustMatrixAuthenticationService( sessionStore: SessionStore = InMemorySessionStore(), + clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(), ): RustMatrixAuthenticationService { val baseDirectory = File("/base") val cacheDirectory = File("/cache") @@ -43,6 +56,7 @@ class RustMatrixAuthenticationServiceTest { baseDirectory = baseDirectory, cacheDirectory = cacheDirectory, sessionStore = sessionStore, + clientBuilderProvider = clientBuilderProvider, ) return RustMatrixAuthenticationService( sessionPathsFactory = SessionPathsFactory(baseDirectory, cacheDirectory), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt index 2922762548..d606008989 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt @@ -30,7 +30,6 @@ fun aRustRoomMember( membership = membership, isNameAmbiguous = isNameAmbiguous, powerLevel = powerLevel, - normalizedPowerLevel = powerLevel, isIgnored = isIgnored, suggestedRoleForPowerLevel = role, membershipChangeReason = membershipChangeReason, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/SpaceRoom.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/SpaceRoom.kt new file mode 100644 index 0000000000..b9f665add1 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/SpaceRoom.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.factories + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import org.matrix.rustcomponents.sdk.JoinRule +import org.matrix.rustcomponents.sdk.Membership +import org.matrix.rustcomponents.sdk.RoomHero +import org.matrix.rustcomponents.sdk.RoomType +import org.matrix.rustcomponents.sdk.SpaceRoom + +fun aRustSpaceRoom( + roomId: RoomId = A_ROOM_ID, + isDirect: Boolean = false, + canonicalAlias: String? = null, + name: String? = null, + topic: String? = null, + avatarUrl: String? = null, + roomType: RoomType = RoomType.Space, + numJoinedMembers: ULong = 0uL, + joinRule: JoinRule? = null, + worldReadable: Boolean? = null, + guestCanJoin: Boolean = false, + childrenCount: ULong = 0uL, + state: Membership? = null, + heroes: List = emptyList(), +) = SpaceRoom( + roomId = roomId.value, + isDirect = isDirect, + canonicalAlias = canonicalAlias, + name = name, + topic = topic, + avatarUrl = avatarUrl, + roomType = roomType, + numJoinedMembers = numJoinedMembers, + joinRule = joinRule, + worldReadable = worldReadable, + guestCanJoin = guestCanJoin, + childrenCount = childrenCount, + state = state, + heroes = heroes, + via = emptyList() +) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt index 8e88fa78fe..3f53d9d5e2 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt @@ -15,6 +15,7 @@ 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.HomeserverLoginDetails import org.matrix.rustcomponents.sdk.IgnoredUsersListener import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.NotificationClient @@ -25,6 +26,7 @@ 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.SpaceService import org.matrix.rustcomponents.sdk.SyncService import org.matrix.rustcomponents.sdk.SyncServiceBuilder import org.matrix.rustcomponents.sdk.TaskHandle @@ -40,6 +42,8 @@ class FakeFfiClient( private val session: Session = aRustSession(), private val clearCachesResult: () -> Unit = { lambdaError() }, private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() }, + private val getProfileResult: (String) -> UserProfile = { UserProfile(userId = userId, displayName = null, avatarUrl = null) }, + private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() }, private val closeResult: () -> Unit = {}, ) : Client(NoPointer) { override fun userId(): String = userId @@ -52,6 +56,7 @@ class FakeFfiClient( override suspend fun cachedAvatarUrl(): String? = null override suspend fun restoreSession(session: Session) = Unit override fun syncService(): SyncServiceBuilder = FakeFfiSyncServiceBuilder() + override fun spaceService(): SpaceService = FakeFfiSpaceService() override fun roomDirectorySearch(): RoomDirectorySearch = FakeFfiRoomDirectorySearch() override suspend fun setPusher( identifiers: PusherIdentifiers, @@ -69,12 +74,18 @@ class FakeFfiClient( 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) + return getProfileResult(userId) } + + override suspend fun homeserverLoginDetails(): HomeserverLoginDetails { + return homeserverLoginDetailsResult() + } + 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 index 6e0c73b350..d2dce3816c 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClientBuilder.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClientBuilder.kt @@ -17,7 +17,9 @@ import uniffi.matrix_sdk.BackupDownloadStrategy import uniffi.matrix_sdk_crypto.CollectStrategy import uniffi.matrix_sdk_crypto.DecryptionSettings -class FakeFfiClientBuilder : ClientBuilder(NoPointer) { +class FakeFfiClientBuilder( + val buildResult: () -> Client = { FakeFfiClient(withUtdHook = {}) } +) : ClientBuilder(NoPointer) { override fun addRootCertificates(certificates: List) = this override fun autoEnableBackups(autoEnableBackups: Boolean) = this override fun autoEnableCrossSigning(autoEnableCrossSigning: Boolean) = this @@ -40,8 +42,5 @@ class FakeFfiClientBuilder : ClientBuilder(NoPointer) { override fun username(username: String) = this override fun enableShareHistoryOnInvite(enableShareHistoryOnInvite: Boolean): ClientBuilder = this override fun threadsEnabled(enabled: Boolean, threadSubscriptions: Boolean): ClientBuilder = this - - override suspend fun build(): Client { - return FakeFfiClient(withUtdHook = {}) - } + override suspend fun build() = buildResult() } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSpaceRoomList.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSpaceRoomList.kt new file mode 100644 index 0000000000..171afd49fc --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSpaceRoomList.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.libraries.matrix.impl.fixtures.fakes + +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.SpaceListUpdate +import org.matrix.rustcomponents.sdk.SpaceRoom +import org.matrix.rustcomponents.sdk.SpaceRoomList +import org.matrix.rustcomponents.sdk.SpaceRoomListEntriesListener +import org.matrix.rustcomponents.sdk.SpaceRoomListPaginationStateListener +import org.matrix.rustcomponents.sdk.TaskHandle +import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState + +class FakeFfiSpaceRoomList( + private val paginateResult: () -> Unit = { lambdaError() }, + private val paginationStateResult: () -> SpaceRoomListPaginationState = { lambdaError() }, + private val roomsResult: () -> List = { lambdaError() }, +) : SpaceRoomList(NoPointer) { + private var spaceRoomListPaginationStateListener: SpaceRoomListPaginationStateListener? = null + private var spaceRoomListEntriesListener: SpaceRoomListEntriesListener? = null + + override suspend fun paginate() = simulateLongTask { + paginateResult() + } + + override fun paginationState(): SpaceRoomListPaginationState { + return paginationStateResult() + } + + override fun rooms(): List { + return roomsResult() + } + + override fun subscribeToPaginationStateUpdates(listener: SpaceRoomListPaginationStateListener): TaskHandle { + spaceRoomListPaginationStateListener = listener + return FakeFfiTaskHandle() + } + + fun triggerPaginationStateUpdate(state: SpaceRoomListPaginationState) { + spaceRoomListPaginationStateListener?.onUpdate(state) + } + + override fun subscribeToRoomUpdate(listener: SpaceRoomListEntriesListener): TaskHandle { + spaceRoomListEntriesListener = listener + return FakeFfiTaskHandle() + } + + fun triggerRoomListUpdate(rooms: List) { + spaceRoomListEntriesListener?.onUpdate(rooms) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSpaceService.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSpaceService.kt new file mode 100644 index 0000000000..3dae78ae1b --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSpaceService.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.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.SpaceService + +class FakeFfiSpaceService : SpaceService(NoPointer) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserProfileMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/mapper/UserProfileMapperTest.kt similarity index 78% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserProfileMapperTest.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/mapper/UserProfileMapperTest.kt index e04b0f26d5..ea2d971056 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserProfileMapperTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/mapper/UserProfileMapperTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.matrix.impl.usersearch +package io.element.android.libraries.matrix.impl.mapper import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.user.MatrixUser @@ -16,7 +16,7 @@ import org.junit.Test class UserProfileMapperTest { @Test fun map() { - assertThat(UserProfileMapper.map(aRustUserProfile(A_USER_ID.value, "displayName", "avatarUrl"))) + assertThat(aRustUserProfile(A_USER_ID.value, "displayName", "avatarUrl").map()) .isEqualTo(MatrixUser(A_USER_ID, "displayName", "avatarUrl")) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventTypeKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventTypeKtTest.kt index 980aba75fa..02e503c56e 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventTypeKtTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventTypeKtTest.kt @@ -15,55 +15,55 @@ import org.matrix.rustcomponents.sdk.MessageLikeEventType class MessageEventTypeKtTest { @Test fun `map Rust type should result to correct Kotlin type`() { - assertThat(MessageLikeEventType.CALL_ANSWER.map()).isEqualTo(MessageEventType.CALL_ANSWER) - assertThat(MessageLikeEventType.CALL_INVITE.map()).isEqualTo(MessageEventType.CALL_INVITE) - assertThat(MessageLikeEventType.CALL_HANGUP.map()).isEqualTo(MessageEventType.CALL_HANGUP) - assertThat(MessageLikeEventType.CALL_CANDIDATES.map()).isEqualTo(MessageEventType.CALL_CANDIDATES) - assertThat(MessageLikeEventType.CALL_NOTIFY.map()).isEqualTo(MessageEventType.CALL_NOTIFY) - assertThat(MessageLikeEventType.KEY_VERIFICATION_READY.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_READY) - assertThat(MessageLikeEventType.KEY_VERIFICATION_START.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_START) - assertThat(MessageLikeEventType.KEY_VERIFICATION_CANCEL.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_CANCEL) - assertThat(MessageLikeEventType.KEY_VERIFICATION_ACCEPT.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_ACCEPT) - assertThat(MessageLikeEventType.KEY_VERIFICATION_KEY.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_KEY) - assertThat(MessageLikeEventType.KEY_VERIFICATION_MAC.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_MAC) - assertThat(MessageLikeEventType.KEY_VERIFICATION_DONE.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_DONE) - assertThat(MessageLikeEventType.REACTION.map()).isEqualTo(MessageEventType.REACTION) - assertThat(MessageLikeEventType.ROOM_ENCRYPTED.map()).isEqualTo(MessageEventType.ROOM_ENCRYPTED) - assertThat(MessageLikeEventType.ROOM_MESSAGE.map()).isEqualTo(MessageEventType.ROOM_MESSAGE) - assertThat(MessageLikeEventType.ROOM_REDACTION.map()).isEqualTo(MessageEventType.ROOM_REDACTION) - assertThat(MessageLikeEventType.STICKER.map()).isEqualTo(MessageEventType.STICKER) - assertThat(MessageLikeEventType.POLL_END.map()).isEqualTo(MessageEventType.POLL_END) - assertThat(MessageLikeEventType.POLL_RESPONSE.map()).isEqualTo(MessageEventType.POLL_RESPONSE) - assertThat(MessageLikeEventType.POLL_START.map()).isEqualTo(MessageEventType.POLL_START) - assertThat(MessageLikeEventType.UNSTABLE_POLL_END.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_END) - assertThat(MessageLikeEventType.UNSTABLE_POLL_RESPONSE.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_RESPONSE) - assertThat(MessageLikeEventType.UNSTABLE_POLL_START.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_START) + assertThat(MessageLikeEventType.CallAnswer.map()).isEqualTo(MessageEventType.CallAnswer) + assertThat(MessageLikeEventType.CallInvite.map()).isEqualTo(MessageEventType.CallInvite) + assertThat(MessageLikeEventType.CallHangup.map()).isEqualTo(MessageEventType.CallHangup) + assertThat(MessageLikeEventType.CallCandidates.map()).isEqualTo(MessageEventType.CallCandidates) + assertThat(MessageLikeEventType.RtcNotification.map()).isEqualTo(MessageEventType.RtcNotification) + assertThat(MessageLikeEventType.KeyVerificationReady.map()).isEqualTo(MessageEventType.KeyVerificationReady) + assertThat(MessageLikeEventType.KeyVerificationStart.map()).isEqualTo(MessageEventType.KeyVerificationStart) + assertThat(MessageLikeEventType.KeyVerificationCancel.map()).isEqualTo(MessageEventType.KeyVerificationCancel) + assertThat(MessageLikeEventType.KeyVerificationAccept.map()).isEqualTo(MessageEventType.KeyVerificationAccept) + assertThat(MessageLikeEventType.KeyVerificationKey.map()).isEqualTo(MessageEventType.KeyVerificationKey) + assertThat(MessageLikeEventType.KeyVerificationMac.map()).isEqualTo(MessageEventType.KeyVerificationMac) + assertThat(MessageLikeEventType.KeyVerificationDone.map()).isEqualTo(MessageEventType.KeyVerificationDone) + assertThat(MessageLikeEventType.Reaction.map()).isEqualTo(MessageEventType.Reaction) + assertThat(MessageLikeEventType.RoomEncrypted.map()).isEqualTo(MessageEventType.RoomEncrypted) + assertThat(MessageLikeEventType.RoomMessage.map()).isEqualTo(MessageEventType.RoomMessage) + assertThat(MessageLikeEventType.RoomRedaction.map()).isEqualTo(MessageEventType.RoomRedaction) + assertThat(MessageLikeEventType.Sticker.map()).isEqualTo(MessageEventType.Sticker) + assertThat(MessageLikeEventType.PollEnd.map()).isEqualTo(MessageEventType.PollEnd) + assertThat(MessageLikeEventType.PollResponse.map()).isEqualTo(MessageEventType.PollResponse) + assertThat(MessageLikeEventType.PollStart.map()).isEqualTo(MessageEventType.PollStart) + assertThat(MessageLikeEventType.UnstablePollEnd.map()).isEqualTo(MessageEventType.UnstablePollEnd) + assertThat(MessageLikeEventType.UnstablePollResponse.map()).isEqualTo(MessageEventType.UnstablePollResponse) + assertThat(MessageLikeEventType.UnstablePollStart.map()).isEqualTo(MessageEventType.UnstablePollStart) } @Test fun `map Kotlin type should result to correct Rust type`() { - assertThat(MessageEventType.CALL_ANSWER.map()).isEqualTo(MessageLikeEventType.CALL_ANSWER) - assertThat(MessageEventType.CALL_INVITE.map()).isEqualTo(MessageLikeEventType.CALL_INVITE) - assertThat(MessageEventType.CALL_HANGUP.map()).isEqualTo(MessageLikeEventType.CALL_HANGUP) - assertThat(MessageEventType.CALL_CANDIDATES.map()).isEqualTo(MessageLikeEventType.CALL_CANDIDATES) - assertThat(MessageEventType.CALL_NOTIFY.map()).isEqualTo(MessageLikeEventType.CALL_NOTIFY) - assertThat(MessageEventType.KEY_VERIFICATION_READY.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_READY) - assertThat(MessageEventType.KEY_VERIFICATION_START.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_START) - assertThat(MessageEventType.KEY_VERIFICATION_CANCEL.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_CANCEL) - assertThat(MessageEventType.KEY_VERIFICATION_ACCEPT.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_ACCEPT) - assertThat(MessageEventType.KEY_VERIFICATION_KEY.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_KEY) - assertThat(MessageEventType.KEY_VERIFICATION_MAC.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_MAC) - assertThat(MessageEventType.KEY_VERIFICATION_DONE.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_DONE) - assertThat(MessageEventType.REACTION.map()).isEqualTo(MessageLikeEventType.REACTION) - assertThat(MessageEventType.ROOM_ENCRYPTED.map()).isEqualTo(MessageLikeEventType.ROOM_ENCRYPTED) - assertThat(MessageEventType.ROOM_MESSAGE.map()).isEqualTo(MessageLikeEventType.ROOM_MESSAGE) - assertThat(MessageEventType.ROOM_REDACTION.map()).isEqualTo(MessageLikeEventType.ROOM_REDACTION) - assertThat(MessageEventType.STICKER.map()).isEqualTo(MessageLikeEventType.STICKER) - assertThat(MessageEventType.POLL_END.map()).isEqualTo(MessageLikeEventType.POLL_END) - assertThat(MessageEventType.POLL_RESPONSE.map()).isEqualTo(MessageLikeEventType.POLL_RESPONSE) - assertThat(MessageEventType.POLL_START.map()).isEqualTo(MessageLikeEventType.POLL_START) - assertThat(MessageEventType.UNSTABLE_POLL_END.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_END) - assertThat(MessageEventType.UNSTABLE_POLL_RESPONSE.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_RESPONSE) - assertThat(MessageEventType.UNSTABLE_POLL_START.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_START) + assertThat(MessageEventType.CallAnswer.map()).isEqualTo(MessageLikeEventType.CallAnswer) + assertThat(MessageEventType.CallInvite.map()).isEqualTo(MessageLikeEventType.CallInvite) + assertThat(MessageEventType.CallHangup.map()).isEqualTo(MessageLikeEventType.CallHangup) + assertThat(MessageEventType.CallCandidates.map()).isEqualTo(MessageLikeEventType.CallCandidates) + assertThat(MessageEventType.RtcNotification.map()).isEqualTo(MessageLikeEventType.RtcNotification) + assertThat(MessageEventType.KeyVerificationReady.map()).isEqualTo(MessageLikeEventType.KeyVerificationReady) + assertThat(MessageEventType.KeyVerificationStart.map()).isEqualTo(MessageLikeEventType.KeyVerificationStart) + assertThat(MessageEventType.KeyVerificationCancel.map()).isEqualTo(MessageLikeEventType.KeyVerificationCancel) + assertThat(MessageEventType.KeyVerificationAccept.map()).isEqualTo(MessageLikeEventType.KeyVerificationAccept) + assertThat(MessageEventType.KeyVerificationKey.map()).isEqualTo(MessageLikeEventType.KeyVerificationKey) + assertThat(MessageEventType.KeyVerificationMac.map()).isEqualTo(MessageLikeEventType.KeyVerificationMac) + assertThat(MessageEventType.KeyVerificationDone.map()).isEqualTo(MessageLikeEventType.KeyVerificationDone) + assertThat(MessageEventType.Reaction.map()).isEqualTo(MessageLikeEventType.Reaction) + assertThat(MessageEventType.RoomEncrypted.map()).isEqualTo(MessageLikeEventType.RoomEncrypted) + assertThat(MessageEventType.RoomMessage.map()).isEqualTo(MessageLikeEventType.RoomMessage) + assertThat(MessageEventType.RoomRedaction.map()).isEqualTo(MessageLikeEventType.RoomRedaction) + assertThat(MessageEventType.Sticker.map()).isEqualTo(MessageLikeEventType.Sticker) + assertThat(MessageEventType.PollEnd.map()).isEqualTo(MessageLikeEventType.PollEnd) + assertThat(MessageEventType.PollResponse.map()).isEqualTo(MessageLikeEventType.PollResponse) + assertThat(MessageEventType.PollStart.map()).isEqualTo(MessageLikeEventType.PollStart) + assertThat(MessageEventType.UnstablePollEnd.map()).isEqualTo(MessageLikeEventType.UnstablePollEnd) + assertThat(MessageEventType.UnstablePollResponse.map()).isEqualTo(MessageLikeEventType.UnstablePollResponse) + assertThat(MessageEventType.UnstablePollStart.map()).isEqualTo(MessageLikeEventType.UnstablePollStart) } } 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 index e24eca53cb..50d6c348b5 100644 --- 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 @@ -57,6 +57,7 @@ class RustBaseRoomTest { leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) { val membershipUpdate = awaitItem() assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId) + assertThat(membershipUpdate.isSpace).isFalse() assertThat(membershipUpdate.isUserInRoom).isFalse() assertThat(membershipUpdate.change).isEqualTo(MembershipChange.LEFT) } @@ -77,6 +78,7 @@ class RustBaseRoomTest { leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) { val membershipUpdate = awaitItem() assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId) + assertThat(membershipUpdate.isSpace).isFalse() assertThat(membershipUpdate.isUserInRoom).isFalse() assertThat(membershipUpdate.change).isEqualTo(MembershipChange.KNOCK_RETRACTED) } @@ -97,6 +99,7 @@ class RustBaseRoomTest { leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) { val membershipUpdate = awaitItem() assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId) + assertThat(membershipUpdate.isSpace).isFalse() assertThat(membershipUpdate.isUserInRoom).isFalse() assertThat(membershipUpdate.change).isEqualTo(MembershipChange.INVITATION_REJECTED) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/spaces/RoomSummaryListProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/spaces/RoomSummaryListProcessorTest.kt new file mode 100644 index 0000000000..ae05c7b4e9 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/spaces/RoomSummaryListProcessorTest.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.spaces + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSpaceRoom +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.previewutils.room.aSpaceRoom +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.rustcomponents.sdk.SpaceListUpdate + +class RoomSummaryListProcessorTest { + private val spaceRoomsFlow = MutableStateFlow>(emptyList()) + + @Test + fun `Append adds new entries at the end of the list`() = runTest { + spaceRoomsFlow.value = listOf(aSpaceRoom()) + val processor = createProcessor() + + val newEntry = aRustSpaceRoom(roomId = A_ROOM_ID_2) + processor.postUpdates(listOf(SpaceListUpdate.Append(listOf(newEntry, newEntry, newEntry)))) + + assertThat(spaceRoomsFlow.value.count()).isEqualTo(4) + assertThat(spaceRoomsFlow.value.subList(1, 4).all { it.roomId == A_ROOM_ID_2 }).isTrue() + } + + @Test + fun `PushBack adds a new entry at the end of the list`() = runTest { + spaceRoomsFlow.value = listOf(aSpaceRoom()) + val processor = createProcessor() + processor.postUpdates(listOf(SpaceListUpdate.PushBack(aRustSpaceRoom(roomId = A_ROOM_ID_2)))) + + assertThat(spaceRoomsFlow.value.count()).isEqualTo(2) + assertThat(spaceRoomsFlow.value.last().roomId).isEqualTo(A_ROOM_ID_2) + } + + @Test + fun `PushFront inserts a new entry at the start of the list`() = runTest { + spaceRoomsFlow.value = listOf(aSpaceRoom()) + val processor = createProcessor() + processor.postUpdates(listOf(SpaceListUpdate.PushFront(aRustSpaceRoom(roomId = A_ROOM_ID_2)))) + + assertThat(spaceRoomsFlow.value.count()).isEqualTo(2) + assertThat(spaceRoomsFlow.value.first().roomId).isEqualTo(A_ROOM_ID_2) + } + + @Test + fun `Set replaces an entry at some index`() = runTest { + spaceRoomsFlow.value = listOf(aSpaceRoom()) + val processor = createProcessor() + val index = 0 + + processor.postUpdates(listOf(SpaceListUpdate.Set(index.toUInt(), aRustSpaceRoom(roomId = A_ROOM_ID_2)))) + + assertThat(spaceRoomsFlow.value.count()).isEqualTo(1) + assertThat(spaceRoomsFlow.value[index].roomId).isEqualTo(A_ROOM_ID_2) + } + + @Test + fun `Insert inserts a new entry at the provided index`() = runTest { + spaceRoomsFlow.value = listOf(aSpaceRoom()) + val processor = createProcessor() + val index = 0 + + processor.postUpdates(listOf(SpaceListUpdate.Insert(index.toUInt(), aRustSpaceRoom(roomId = A_ROOM_ID_2)))) + + assertThat(spaceRoomsFlow.value.count()).isEqualTo(2) + assertThat(spaceRoomsFlow.value[index].roomId).isEqualTo(A_ROOM_ID_2) + } + + @Test + fun `Remove removes an entry at some index`() = runTest { + spaceRoomsFlow.value = listOf( + aSpaceRoom(roomId = A_ROOM_ID), + aSpaceRoom(roomId = A_ROOM_ID_2) + ) + val processor = createProcessor() + val index = 0 + + processor.postUpdates(listOf(SpaceListUpdate.Remove(index.toUInt()))) + + assertThat(spaceRoomsFlow.value.count()).isEqualTo(1) + assertThat(spaceRoomsFlow.value[index].roomId).isEqualTo(A_ROOM_ID_2) + } + + @Test + fun `PopBack removes an entry at the end of the list`() = runTest { + spaceRoomsFlow.value = listOf( + aSpaceRoom(roomId = A_ROOM_ID), + aSpaceRoom(roomId = A_ROOM_ID_2) + ) + val processor = createProcessor() + val index = 0 + + processor.postUpdates(listOf(SpaceListUpdate.PopBack)) + + assertThat(spaceRoomsFlow.value.count()).isEqualTo(1) + assertThat(spaceRoomsFlow.value[index].roomId).isEqualTo(A_ROOM_ID) + } + + @Test + fun `PopFront removes an entry at the start of the list`() = runTest { + spaceRoomsFlow.value = listOf( + aSpaceRoom(roomId = A_ROOM_ID), + aSpaceRoom(roomId = A_ROOM_ID_2) + ) + val processor = createProcessor() + val index = 0 + + processor.postUpdates(listOf(SpaceListUpdate.PopFront)) + + assertThat(spaceRoomsFlow.value.count()).isEqualTo(1) + assertThat(spaceRoomsFlow.value[index].roomId).isEqualTo(A_ROOM_ID_2) + } + + @Test + fun `Clear removes all the entries`() = runTest { + spaceRoomsFlow.value = listOf( + aSpaceRoom(roomId = A_ROOM_ID), + aSpaceRoom(roomId = A_ROOM_ID_2) + ) + val processor = createProcessor() + + processor.postUpdates(listOf(SpaceListUpdate.Clear)) + + assertThat(spaceRoomsFlow.value).isEmpty() + } + + @Test + fun `Truncate removes all entries after the provided length`() = runTest { + spaceRoomsFlow.value = listOf( + aSpaceRoom(roomId = A_ROOM_ID), + aSpaceRoom(roomId = A_ROOM_ID_2) + ) + val processor = createProcessor() + val index = 0 + + processor.postUpdates(listOf(SpaceListUpdate.Truncate(1u))) + + assertThat(spaceRoomsFlow.value.count()).isEqualTo(1) + assertThat(spaceRoomsFlow.value[index].roomId).isEqualTo(A_ROOM_ID) + } + + @Test + fun `Reset removes all entries and add the provided ones`() = runTest { + spaceRoomsFlow.value = listOf( + aSpaceRoom(roomId = A_ROOM_ID), + aSpaceRoom(roomId = A_ROOM_ID_2) + ) + val processor = createProcessor() + val index = 0 + + processor.postUpdates(listOf(SpaceListUpdate.Reset(listOf(aRustSpaceRoom(A_ROOM_ID_3))))) + + assertThat(spaceRoomsFlow.value.count()).isEqualTo(1) + assertThat(spaceRoomsFlow.value[index].roomId).isEqualTo(A_ROOM_ID_3) + } + + @Test + fun `When there is no replay cache SpaceListUpdateProcessor starts with an empty list`() = runTest { + val spaceRoomsSharedFlow = MutableSharedFlow>(replay = 1) + val processor = createProcessor( + spaceRoomsFlow = spaceRoomsSharedFlow, + ) + assertThat(spaceRoomsSharedFlow.replayCache).isEmpty() + val newEntry = aRustSpaceRoom(roomId = A_ROOM_ID) + processor.postUpdates(listOf(SpaceListUpdate.Append(listOf(newEntry)))) + assertThat(spaceRoomsSharedFlow.replayCache).hasSize(1) + assertThat(spaceRoomsSharedFlow.replayCache.first()).hasSize(1) + } + + private fun createProcessor( + spaceRoomsFlow: MutableSharedFlow> = this.spaceRoomsFlow, + ) = SpaceListUpdateProcessor( + spaceRoomsFlow = spaceRoomsFlow, + mapper = SpaceRoomMapper(), + ) +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceRoomListTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceRoomListTest.kt new file mode 100644 index 0000000000..7a494ae8c3 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceRoomListTest.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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.libraries.matrix.impl.spaces + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSpaceRoom +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSpaceRoomList +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.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.rustcomponents.sdk.SpaceListUpdate +import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState +import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList + +class RustSpaceRoomListTest { + @Test + fun `paginationStatusFlow emits values`() = runTest { + val innerSpaceRoomList = FakeFfiSpaceRoomList( + paginationStateResult = { SpaceRoomListPaginationState.Idle(false) } + ) + val sut = createRustSpaceRoomList( + innerSpaceRoomList = innerSpaceRoomList, + ) + sut.paginationStatusFlow.test { + // First value is the initial one + assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false)) + // First value after the subscription occurs + assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = true)) + innerSpaceRoomList.triggerPaginationStateUpdate(SpaceRoomListPaginationState.Loading) + assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Loading) + innerSpaceRoomList.triggerPaginationStateUpdate(SpaceRoomListPaginationState.Idle(true)) + assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false)) + innerSpaceRoomList.triggerPaginationStateUpdate(SpaceRoomListPaginationState.Idle(false)) + assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = true)) + } + } + + @Test + fun `spaceRoomsFlow emits values`() = runTest { + val innerSpaceRoomList = FakeFfiSpaceRoomList( + paginationStateResult = { SpaceRoomListPaginationState.Idle(false) } + ) + val sut = createRustSpaceRoomList( + innerSpaceRoomList = innerSpaceRoomList, + ) + sut.spaceRoomsFlow.test { + // Give time for the subscription to be set + runCurrent() + innerSpaceRoomList.triggerRoomListUpdate( + listOf( + SpaceListUpdate.PushBack(aRustSpaceRoom(roomId = A_ROOM_ID_2)) + ) + ) + val rooms = awaitItem() + assertThat(rooms).hasSize(1) + assertThat(rooms[0].roomId).isEqualTo(A_ROOM_ID_2) + } + } + + @Test + fun `paginate invokes paginate on the inner class`() = runTest { + val paginateResult = lambdaRecorder { } + val innerSpaceRoomList = FakeFfiSpaceRoomList( + paginateResult = paginateResult, + ) + val sut = createRustSpaceRoomList( + innerSpaceRoomList = innerSpaceRoomList, + ) + sut.paginate() + paginateResult.assertions().isCalledOnce() + } + + private fun TestScope.createRustSpaceRoomList( + roomId: RoomId = A_ROOM_ID, + innerSpaceRoomList: InnerSpaceRoomList = FakeFfiSpaceRoomList(), + innerProvider: suspend () -> InnerSpaceRoomList = { innerSpaceRoomList }, + spaceRoomMapper: SpaceRoomMapper = SpaceRoomMapper(), + ): RustSpaceRoomList { + return RustSpaceRoomList( + roomId = roomId, + innerProvider = innerProvider, + coroutineScope = backgroundScope, + spaceRoomMapper = spaceRoomMapper, + ) + } +} 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 c92fb9776b..881d718392 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 @@ -24,7 +24,6 @@ class LoadingIndicatorsPostProcessorTest { items = listOf(messageEvent, messageEvent2), hasMoreToLoadBackward = true, hasMoreToLoadForward = false, - timelineMode = Timeline.Mode.Live, ) assertThat(result).containsExactly( MatrixTimelineItem.Virtual( @@ -47,7 +46,6 @@ class LoadingIndicatorsPostProcessorTest { items = listOf(messageEvent, messageEvent2), hasMoreToLoadBackward = false, hasMoreToLoadForward = true, - timelineMode = Timeline.Mode.Live, ) assertThat(result).containsExactly( messageEvent, @@ -70,7 +68,6 @@ class LoadingIndicatorsPostProcessorTest { items = listOf(messageEvent, messageEvent2), hasMoreToLoadBackward = true, hasMoreToLoadForward = true, - timelineMode = Timeline.Mode.Live, ) assertThat(result).containsExactly( MatrixTimelineItem.Virtual( @@ -100,7 +97,6 @@ class LoadingIndicatorsPostProcessorTest { items = listOf(), hasMoreToLoadBackward = true, hasMoreToLoadForward = true, - timelineMode = Timeline.Mode.Live, ) assertThat(result).containsExactly( MatrixTimelineItem.Virtual( diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/util/SessionPathsProviderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/util/SessionPathsProviderTest.kt index c48e98eb6b..43e5a98e50 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/util/SessionPathsProviderTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/util/SessionPathsProviderTest.kt @@ -9,7 +9,7 @@ package io.element.android.libraries.matrix.impl.util import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData import kotlinx.coroutines.test.runTest import org.junit.Test @@ -24,14 +24,15 @@ class SessionPathsProviderTest { @Test fun `if session is found, provides returns the data`() = runTest { - val store = InMemorySessionStore() - val sut = SessionPathsProvider(store) - store.storeData( - aSessionData( - sessionPath = "/a/path/to/a/session", - cachePath = "/a/path/to/a/cache", + val store = InMemorySessionStore( + initialList = listOf( + aSessionData( + sessionPath = "/a/path/to/a/session", + cachePath = "/a/path/to/a/cache", + ) ) ) + val sut = SessionPathsProvider(store) val result = sut.provides(A_SESSION_ID)!! assertThat(result.fileDirectory.absolutePath).isEqualTo("/a/path/to/a/session") assertThat(result.cacheDirectory.absolutePath).isEqualTo("/a/path/to/a/cache") diff --git a/libraries/matrix/test/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 dfc82e132d..67e7561972 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 @@ -30,6 +30,7 @@ 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.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.spaces.SpaceService 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 @@ -42,6 +43,7 @@ import io.element.android.libraries.matrix.test.notificationsettings.FakeNotific import io.element.android.libraries.matrix.test.pushers.FakePushersService import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService +import io.element.android.libraries.matrix.test.spaces.FakeSpaceService import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.tests.testutils.lambda.lambdaError @@ -65,6 +67,7 @@ class FakeMatrixClient( private val userDisplayName: String? = A_USER_NAME, private val userAvatarUrl: String? = AN_AVATAR_URL, override val roomListService: RoomListService = FakeRoomListService(), + override val spaceService: SpaceService = FakeSpaceService(), override val mediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(), private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), private val pushersService: FakePushersService = FakePushersService(), @@ -87,7 +90,6 @@ class FakeMatrixClient( private val canDeactivateAccountResult: () -> Boolean = { lambdaError() }, private val deactivateAccountResult: (String, Boolean) -> Result = { _, _ -> lambdaError() }, private val currentSlidingSyncVersionLambda: () -> 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 }, @@ -95,6 +97,8 @@ class FakeMatrixClient( override val ignoredUsersFlow: StateFlow> = MutableStateFlow(persistentListOf()), private val getMaxUploadSizeResult: () -> Result = { lambdaError() }, private val getJoinedRoomIdsResult: () -> Result> = { Result.success(emptySet()) }, + private val getRecentEmojisLambda: () -> Result> = { Result.success(emptyList()) }, + private val addRecentEmojiLambda: (String) -> Result = { Result.success(Unit) }, ) : MatrixClient { var setDisplayNameCalled: Boolean = false private set @@ -336,10 +340,6 @@ class FakeMatrixClient( return currentSlidingSyncVersionLambda() } - override suspend fun availableSlidingSyncVersions(): Result> { - return availableSlidingSyncVersionsLambda() - } - override suspend fun canReportRoom(): Boolean { return canReportRoomLambda() } @@ -351,4 +351,12 @@ class FakeMatrixClient( override suspend fun getMaxFileUploadSize(): Result { return getMaxUploadSizeResult() } + + override suspend fun addRecentEmoji(emoji: String): Result { + return addRecentEmojiLambda(emoji) + } + + override suspend fun getRecentEmojis(): Result> { + return getRecentEmojisLambda() + } } 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 a63c301ce0..ed18a5ebd9 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 @@ -25,6 +25,7 @@ const val A_USER_NAME_2 = "Bob" const val A_PASSWORD = "password" const val A_PASSPHRASE = "passphrase" const val A_SECRET = "secret" +const val AN_APPLICATION_NAME = "AppName" val A_USER_ID = UserId("@alice:server.org") val A_USER_ID_2 = UserId("@bob:server.org") @@ -64,6 +65,8 @@ const val ANOTHER_MESSAGE = "Hello universe!" const val A_CAPTION = "A media caption" const val A_REASON = "A reason" +const val A_SPACE_NAME = "A space name" + const val A_REDACTION_REASON = "A redaction reason" const val A_HOMESERVER_URL = "matrix.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 f236c37da8..f1554df1d0 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 @@ -19,14 +19,11 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.simulateLongTask -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flowOf val A_OIDC_DATA = OidcDetails(url = "a-url") @@ -44,14 +41,6 @@ class FakeMatrixAuthenticationService( private var matrixClient: MatrixClient? = null private var onAuthenticationListener: ((MatrixClient) -> Unit)? = null - var getLatestSessionIdLambda: (() -> SessionId?) = { null } - - override fun loggedInStateFlow(): Flow { - return flowOf(LoggedInState.NotLoggedIn) - } - - override suspend fun getLatestSessionId(): SessionId? = getLatestSessionIdLambda() - override suspend fun restoreSession(sessionId: SessionId): Result { matrixClientResult?.let { return it.invoke(sessionId) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt index 872481a860..31309beecf 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt @@ -29,6 +29,8 @@ 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.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.test.TestScope @@ -59,7 +61,7 @@ class FakeBaseRoom( private val markAsReadResult: (ReceiptType) -> Result = { Result.success(Unit) }, private val powerLevelsResult: () -> Result = { lambdaError() }, private val leaveRoomLambda: () -> Result = { lambdaError() }, - private val updateMembersResult: () -> Unit = { lambdaError() }, + private var 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) }, @@ -69,6 +71,7 @@ class FakeBaseRoom( private val forgetResult: () -> Result = { lambdaError() }, private val reportRoomResult: (String?) -> Result = { lambdaError() }, private val predecessorRoomResult: () -> PredecessorRoom? = { null }, + private val threadRootIdForEventResult: (EventId) -> Result = { lambdaError() }, ) : BaseRoom { private val _roomInfoFlow: MutableStateFlow = MutableStateFlow(initialRoomInfo) override val roomInfoFlow: StateFlow = _roomInfoFlow @@ -77,6 +80,12 @@ class FakeBaseRoom( _roomInfoFlow.tryEmit(roomInfo) } + private val declineCallFlowMap: MutableMap> = mutableMapOf() + + suspend fun givenDecliner(userId: UserId, forNotificationEventId: EventId) { + declineCallFlowMap[forNotificationEventId]?.emit(userId) + } + override val membersStateFlow: MutableStateFlow = MutableStateFlow(RoomMembersState.Unknown) override suspend fun updateMembers() = updateMembersResult() @@ -222,7 +231,24 @@ class FakeBaseRoom( override suspend fun reportRoom(reason: String?) = reportRoomResult(reason) + override suspend fun declineCall(notificationEventId: EventId): Result { + return Result.success(Unit) + } + + override suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow { + val flow = declineCallFlowMap.getOrPut(notificationEventId, { MutableSharedFlow() }) + return flow + } + override fun predecessorRoom(): PredecessorRoom? = predecessorRoomResult() + + fun givenUpdateMembersResult(result: () -> Unit) { + updateMembersResult = result + } + + override suspend fun threadRootIdForEvent(eventId: EventId): Result { + return threadRootIdForEventResult(eventId) + } } fun defaultRoomPowerLevelValues() = RoomPowerLevelsValues( 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 e2644cdf4d..9dc72247f0 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 @@ -19,7 +19,6 @@ fun aRoomMember( membership: RoomMembershipState = RoomMembershipState.JOIN, isNameAmbiguous: Boolean = false, powerLevel: Long = 0L, - normalizedPowerLevel: Long = 0L, isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, @@ -30,7 +29,6 @@ fun aRoomMember( membership = membership, isNameAmbiguous = isNameAmbiguous, powerLevel = powerLevel, - normalizedPowerLevel = normalizedPowerLevel, isIgnored = isIgnored, role = role, membershipChangeReason = membershipChangeReason, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceRoomList.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceRoomList.kt new file mode 100644 index 0000000000..6eac3dd0f4 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceRoomList.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.matrix.test.spaces + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.util.Optional + +class FakeSpaceRoomList( + override val roomId: RoomId = A_ROOM_ID, + initialSpaceFlowValue: SpaceRoom? = null, + initialSpaceRoomsValue: List = emptyList(), + initialSpaceRoomList: SpaceRoomList.PaginationStatus = SpaceRoomList.PaginationStatus.Loading, + private val paginateResult: () -> Result = { lambdaError() }, +) : SpaceRoomList { + private val currentSpaceMutableStateFlow: MutableStateFlow> = MutableStateFlow(Optional.ofNullable(initialSpaceFlowValue)) + override val currentSpaceFlow: StateFlow> = currentSpaceMutableStateFlow.asStateFlow() + + fun emitCurrentSpace(value: SpaceRoom?) { + currentSpaceMutableStateFlow.value = Optional.ofNullable(value) + } + + private val _spaceRoomsFlow: MutableStateFlow> = MutableStateFlow(initialSpaceRoomsValue) + override val spaceRoomsFlow: Flow> = _spaceRoomsFlow.asStateFlow() + + fun emitSpaceRooms(value: List) { + _spaceRoomsFlow.value = value + } + + private val _paginationStatusFlow: MutableStateFlow = MutableStateFlow(initialSpaceRoomList) + override val paginationStatusFlow: StateFlow = _paginationStatusFlow.asStateFlow() + + fun emitPaginationStatus(value: SpaceRoomList.PaginationStatus) { + _paginationStatusFlow.value = value + } + + override suspend fun paginate(): Result = simulateLongTask { + paginateResult() + } + + override fun destroy() { + // No op + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt new file mode 100644 index 0000000000..43cc8ae8d2 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.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.libraries.matrix.test.spaces + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList +import io.element.android.libraries.matrix.api.spaces.SpaceService +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +class FakeSpaceService( + private val joinedSpacesResult: () -> Result> = { lambdaError() }, + private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() }, +) : SpaceService { + private val _spaceRoomsFlow = MutableSharedFlow>() + override val spaceRoomsFlow: SharedFlow> + get() = _spaceRoomsFlow.asSharedFlow() + + suspend fun emitSpaceRoomList(value: List) { + _spaceRoomsFlow.emit(value) + } + + override suspend fun joinedSpaces(): Result> = simulateLongTask { + return joinedSpacesResult() + } + + override fun spaceRoomList(id: RoomId): SpaceRoomList { + return spaceRoomListResult(id) + } +} 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 42a0ecbcb6..6ebd9f9f50 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 @@ -304,9 +304,9 @@ class FakeTimeline( ) } - var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result = { _, _ -> lambdaError() } + var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result = { _, _ -> lambdaError() } - override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result = simulateLongTask { + override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result = simulateLongTask { toggleReactionLambda( emoji, eventOrTransactionId, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt index a082f6c3e1..b564c21b30 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt @@ -104,7 +104,7 @@ fun aMessageContent( body: String = "body", inReplyTo: InReplyTo? = null, isEdited: Boolean = false, - threadInfo: EventThreadInfo = EventThreadInfo(threadRootId = null, threadSummary = null), + threadInfo: EventThreadInfo? = null, messageType: MessageType = TextMessageType( body = body, formatted = null 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 d052a3d9b8..6caa6e1570 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 @@ -11,12 +11,18 @@ import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings class FakeCallWidgetSettingsProvider( - private val provideFn: (String, String, Boolean, Boolean) -> MatrixWidgetSettings = { _, _, _, _ -> MatrixWidgetSettings("id", true, "url") } + private val provideFn: (String, String, Boolean, Boolean, Boolean) -> MatrixWidgetSettings = { _, _, _, _, _ -> MatrixWidgetSettings("id", true, "url") } ) : CallWidgetSettingsProvider { val providedBaseUrls = mutableListOf() - override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean, direct: Boolean): MatrixWidgetSettings { + override suspend fun provide( + baseUrl: String, + widgetId: String, + encrypted: Boolean, + direct: Boolean, + hasActiveCall: Boolean + ): MatrixWidgetSettings { providedBaseUrls += baseUrl - return provideFn(baseUrl, widgetId, encrypted, direct) + return provideFn(baseUrl, widgetId, encrypted, direct, hasActiveCall) } } diff --git a/libraries/matrixui/build.gradle.kts b/libraries/matrixui/build.gradle.kts index 3b54a0224b..96045f3a93 100644 --- a/libraries/matrixui/build.gradle.kts +++ b/libraries/matrixui/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.di) @@ -36,18 +37,10 @@ dependencies { implementation(libs.coil.gif) implementation(libs.coil.network.okhttp) implementation(libs.jsoup) + implementation(projects.libraries.previewutils) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.dateformatter.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.test.mockk) - testImplementation(libs.test.robolectric) - testImplementation(libs.androidx.compose.ui.test.junit) testImplementation(projects.libraries.sessionStorage.test) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableOrgAvatar.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableOrgAvatar.kt new file mode 100644 index 0000000000..433e036d97 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableOrgAvatar.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.unit.LayoutDirection +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.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.text.toPx +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.ui.strings.CommonStrings + +/** + * Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=3643-2678&m=dev + */ +@Composable +fun EditableOrgAvatar( + avatarData: AvatarData, + onEdit: () -> Unit, + modifier: Modifier = Modifier, +) { + val actionEdit = stringResource(id = CommonStrings.action_edit) + val description = stringResource(CommonStrings.a11y_avatar) + Box( + modifier = modifier + .width(avatarData.size.dp + 16.dp) + .clearAndSetSemantics { + contentDescription = description + // Note: this does not set the click effect to the whole Box + // when talkback is not enabled + onClick( + label = actionEdit, + action = { + onEdit() + true + } + ) + } + ) { + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + val editIconRadius = 17.dp.toPx() + val editIconXOffset = 7.dp.toPx() + val editIconYOffset = 15.dp.toPx() + Avatar( + avatarData = avatarData, + avatarType = AvatarType.Space(false), + modifier = Modifier + .align(Alignment.Center) + .graphicsLayer { + compositingStrategy = CompositingStrategy.Offscreen + } + .drawWithContent { + drawContent() + val xOffset = if (isRtl) { + editIconXOffset + } else { + size.width - editIconXOffset + } + drawCircle( + color = Color.Black, + center = Offset( + x = xOffset, + y = size.height - editIconYOffset, + ), + radius = editIconRadius, + blendMode = BlendMode.Clear, + ) + }, + ) + Surface( + color = ElementTheme.colors.bgCanvasDefault, + shape = CircleShape, + border = BorderStroke(1.dp, color = ElementTheme.colors.borderInteractiveSecondary), + modifier = Modifier + .clip(CircleShape) + .size(30.dp) + .align(Alignment.BottomEnd) + .clickable( + indication = ripple(), + interactionSource = remember { MutableInteractionSource() }, + onClick = onEdit, + ), + ) { + Icon( + imageVector = CompoundIcons.Edit(), + // Note: keep the context description for the test + contentDescription = stringResource(id = CommonStrings.action_edit), + tint = ElementTheme.colors.iconPrimary, + modifier = Modifier.padding(6.dp) + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun EditableOrgAvatarPreview() = ElementPreview { + EditableOrgAvatar( + avatarData = anAvatarData( + url = "anUrl", + size = AvatarSize.OrganizationHeader, + ), + onEdit = {}, + ) +} + +@PreviewsDayNight +@Composable +internal fun EditableOrgAvatarRtlPreview() = CompositionLocalProvider( + LocalLayoutDirection provides LayoutDirection.Rtl, +) { + ElementPreview { + EditableOrgAvatar( + avatarData = anAvatarData( + url = "anUrl", + size = AvatarSize.OrganizationHeader, + ), + onEdit = {}, + ) + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/JoinButton.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/JoinButton.kt new file mode 100644 index 0000000000..0d5b94dc64 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/JoinButton.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.ui.components + +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ButtonSize +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun JoinButton( + showProgress: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textActionAccent) { + TextButton( + modifier = modifier, + text = stringResource(CommonStrings.action_join), + onClick = onClick, + size = ButtonSize.LargeLowPadding, + showProgress = showProgress, + ) + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/OrganizationHeader.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/OrganizationHeader.kt new file mode 100644 index 0000000000..ebd5b5969c --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/OrganizationHeader.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.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 + +/** + * Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=3643-2048&m=dev + */ +@Composable +fun OrganizationHeader( + avatarData: AvatarData, + name: String, + numberOfSpaces: Int, + numberOfRooms: Int, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(top = 16.dp, bottom = 24.dp, start = 16.dp, end = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Avatar( + avatarData = avatarData, + avatarType = AvatarType.Space(false), + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = name, + style = ElementTheme.typography.fontHeadingLgBold, + color = ElementTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(12.dp)) + SpaceInfoRow( + leftText = numberOfSpaces(numberOfSpaces), + rightText = numberOfRooms(numberOfRooms), + ) + } +} + +@PreviewsDayNight +@Composable +internal fun OrganizationHeaderPreview() = ElementPreview { + OrganizationHeader( + avatarData = anAvatarData( + url = "anUrl", + size = AvatarSize.OrganizationHeader, + ), + name = "Space name", + numberOfSpaces = 9, + numberOfRooms = 88, + ) +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceHeaderRootView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceHeaderRootView.kt new file mode 100644 index 0000000000..22ef102946 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceHeaderRootView.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.libraries.matrix.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.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.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.ui.strings.CommonStrings + +/** + * Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=3643-2048 + */ +@Composable +fun SpaceHeaderRootView( + numberOfSpaces: Int, + numberOfRooms: Int, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(top = 32.dp, bottom = 24.dp, start = 16.dp, end = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + BigIcon( + style = BigIcon.Style.Default(CompoundIcons.WorkspaceSolid()) + ) + Text( + text = stringResource(CommonStrings.screen_space_list_title), + style = ElementTheme.typography.fontHeadingLgBold, + color = ElementTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ) + SpaceInfoRow( + leftText = numberOfSpaces(numberOfSpaces), + rightText = numberOfRooms(numberOfRooms), + ) + Text( + text = stringResource(CommonStrings.screen_space_list_description), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun SpaceHeaderRootViewPreview() = ElementPreview { + SpaceHeaderRootView( + numberOfSpaces = 3, + numberOfRooms = 10, + ) +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceHeaderView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceHeaderView.kt new file mode 100644 index 0000000000..6dbe7ebf72 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceHeaderView.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom +import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom +import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism +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.matrix.api.room.join.JoinRule +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 + +/** + * Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=3643-2429&m=dev + */ +@Composable +fun SpaceHeaderView( + avatarData: AvatarData, + name: String?, + topic: String?, + joinRule: JoinRule?, + heroes: ImmutableList, + numberOfMembers: Int, + numberOfRooms: Int, + modifier: Modifier = Modifier, + topicMaxLines: Int = Int.MAX_VALUE, +) { + RoomPreviewOrganism( + modifier = modifier.padding(24.dp), + avatar = { + Avatar( + avatarData = avatarData, + avatarType = AvatarType.Space(), + ) + }, + title = { + if (name != null) { + RoomPreviewTitleAtom(title = name) + } else { + RoomPreviewTitleAtom( + title = stringResource(id = CommonStrings.common_no_space_name), + fontStyle = FontStyle.Italic + ) + } + }, + subtitle = { + if (joinRule != null) { + SpaceInfoRow( + joinRule = joinRule, + numberOfRooms = numberOfRooms, + ) + } + }, + description = if (topic.isNullOrBlank()) { + null + } else { + { RoomPreviewDescriptionAtom(description = topic, maxLines = topicMaxLines) } + }, + memberCount = { + SpaceMembersView( + heroes = heroes, + numberOfMembers = numberOfMembers, + modifier = Modifier.padding(horizontal = 32.dp), + ) + }, + ) +} + +@PreviewsDayNight +@Composable +internal fun SpaceHeaderViewPreview() = ElementPreview { + SpaceHeaderView( + avatarData = anAvatarData( + url = "anUrl", + size = AvatarSize.SpaceHeader, + ), + name = "Space name", + topic = "Space topic: " + LoremIpsum(40).values.first(), + topicMaxLines = 2, + joinRule = JoinRule.Public, + heroes = persistentListOf( + aMatrixUser(id = "@1:d", displayName = "Alice", avatarUrl = "aUrl"), + aMatrixUser(id = "@2:d", displayName = "Bob"), + aMatrixUser(id = "@3:d", displayName = "Charlie", avatarUrl = "aUrl"), + aMatrixUser(id = "@4:d", displayName = "Dave"), + ), + numberOfMembers = 999, + numberOfRooms = 10, + ) +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceInfoRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceInfoRow.kt new file mode 100644 index 0000000000..3d973d97ae --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceInfoRow.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.matrix.ui.components + +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.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.matrix.api.room.join.JoinRule +import io.element.android.libraries.ui.strings.CommonPlurals +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun SpaceInfoRow( + leftText: String, + rightText: String, + modifier: Modifier = Modifier, + iconVector: ImageVector? = null, +) { + Row( + modifier = modifier, + horizontalArrangement = spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (iconVector != null) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = iconVector, + contentDescription = null, + tint = ElementTheme.colors.iconTertiary, + ) + } + val text = stringResource(id = CommonStrings.screen_space_list_details, leftText, rightText) + Text( + text = text, + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +fun SpaceInfoRow( + joinRule: JoinRule, + numberOfRooms: Int, + modifier: Modifier = Modifier, +) { + val (leftText, rightText, icon) = when (joinRule) { + JoinRule.Public -> Triple( + stringResource(id = CommonStrings.common_public_space), + numberOfRooms(numberOfRooms), + CompoundIcons.Public(), + ) + // TODO External space + // JoinRule.Private -> Triple( + // stringResource(id = CommonStrings.common_external_space), + // numberOfRooms(numberOfRooms), + // CompoundIcons.Guest(), + // ) + // JoinRule.Private, + else -> Triple( + stringResource(id = CommonStrings.common_private_space), + numberOfRooms(numberOfRooms), + CompoundIcons.Lock(), + ) + } + SpaceInfoRow( + leftText = leftText, + rightText = rightText, + modifier = modifier, + iconVector = icon, + ) +} + +@Composable +@ReadOnlyComposable +fun numberOfRooms(numberOfRooms: Int): String { + return pluralStringResource(CommonPlurals.common_rooms, numberOfRooms, numberOfRooms) +} + +@Composable +@ReadOnlyComposable +fun numberOfSpaces(numberOfSpaces: Int): String { + return pluralStringResource(CommonPlurals.common_spaces, numberOfSpaces, numberOfSpaces) +} + +@PreviewsDayNight +@Composable +internal fun SpaceInfoRowPreview() = ElementPreview { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SpaceInfoRow( + leftText = numberOfSpaces(5), + rightText = numberOfRooms(10), + ) + SpaceInfoRow( + leftText = "Element space", + rightText = numberOfRooms(16), + iconVector = CompoundIcons.Workspace(), + ) + SpaceInfoRow( + joinRule = JoinRule.Private, + numberOfRooms = 4, + ) + SpaceInfoRow( + joinRule = JoinRule.Public, + numberOfRooms = 10, + ) + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceMembersView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceMembersView.kt new file mode 100644 index 0000000000..0c7bc9a37d --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceMembersView.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.atomic.molecules.MembersCountMolecule +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarRow +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.user.MatrixUser +import io.element.android.libraries.matrix.ui.model.getAvatarData +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +/** + * Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=3729-605&m=dev + */ +@Composable +fun SpaceMembersView( + heroes: ImmutableList, + numberOfMembers: Int, + modifier: Modifier = Modifier, +) { + if (heroes.isEmpty()) { + MembersCountMolecule( + memberCount = numberOfMembers, + modifier = modifier, + ) + } else { + SpaceMembersWithAvatar( + heroes = heroes + .take(3) + .map { + it.getAvatarData(AvatarSize.SpaceMember) + } + .toImmutableList(), + numberOfMembers = numberOfMembers, + modifier = modifier, + ) + } +} + +@Composable +private fun SpaceMembersWithAvatar( + heroes: ImmutableList, + numberOfMembers: Int, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + AvatarRow( + avatarDataList = heroes, + avatarType = AvatarType.User, + lastOnTop = true, + ) + Text( + text = "$numberOfMembers", + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + ) + } +} + +@Composable +@PreviewsDayNight +internal fun SpaceMembersViewNoHeroesPreview() = ElementPreview { + SpaceMembersView( + heroes = persistentListOf(), + numberOfMembers = 123, + ) +} + +@Composable +@PreviewsDayNight +internal fun SpaceMembersViewPreview() = ElementPreview( + drawableFallbackForImages = CommonDrawables.sample_avatar, +) { + SpaceMembersView( + heroes = persistentListOf( + aMatrixUser(id = "@1:d", displayName = "Alice", avatarUrl = "aUrl"), + aMatrixUser(id = "@2:d", displayName = "Bob"), + aMatrixUser(id = "@3:d", displayName = "Charlie", avatarUrl = "aUrl"), + aMatrixUser(id = "@4:d", displayName = "Dave"), + ), + numberOfMembers = 123, + ) +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt new file mode 100644 index 0000000000..1abf0ad95e --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt @@ -0,0 +1,275 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +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.atomic.atoms.UnreadIndicatorAtom +import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule +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.onKeyboardContextMenuAction +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.Text +import io.element.android.libraries.designsystem.theme.unreadIndicator +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.join.JoinRule +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.ui.strings.CommonPlurals +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun SpaceRoomItemView( + spaceRoom: SpaceRoom, + showUnreadIndicator: Boolean, + hideAvatars: Boolean, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, + trailingAction: @Composable (() -> Unit)? = null, + bottomAction: @Composable (() -> Unit)? = null, +) { + SpaceRoomItemScaffold( + modifier = modifier, + avatarData = spaceRoom.getAvatarData(AvatarSize.SpaceListItem), + isSpace = spaceRoom.isSpace, + hideAvatars = hideAvatars, + onClick = onClick, + onLongClick = onLongClick, + trailingAction = trailingAction, + ) { + NameAndIndicatorRow( + isSpace = spaceRoom.isSpace, + name = spaceRoom.name, + showIndicator = showUnreadIndicator + ) + Spacer(modifier = Modifier.height(1.dp)) + SubtitleRow( + visibilityIcon = spaceRoom.visibilityIcon(), + subtitle = spaceRoom.subtitle() + ) + Spacer(modifier = Modifier.height(1.dp)) + val info = spaceRoom.info() + if (info.isNotBlank()) { + Text( + modifier = Modifier.weight(1f), + style = ElementTheme.typography.fontBodyMdRegular, + text = info, + fontStyle = FontStyle.Italic.takeIf { spaceRoom.name == null }, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + if (bottomAction != null) { + Spacer(modifier = Modifier.height(12.dp)) + bottomAction() + } + } +} + +@Composable +private fun SubtitleRow( + visibilityIcon: ImageVector?, + subtitle: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + if (visibilityIcon != null) { + Icon( + modifier = Modifier + .size(16.dp) + .padding(end = 4.dp), + imageVector = visibilityIcon, + contentDescription = null, + tint = ElementTheme.colors.iconTertiary, + ) + } + Text( + modifier = Modifier.weight(1f), + style = ElementTheme.typography.fontBodyMdRegular, + text = subtitle, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Composable +private fun NameAndIndicatorRow( + isSpace: Boolean, + name: String?, + showIndicator: Boolean, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + style = ElementTheme.typography.fontBodyLgMedium, + text = name ?: stringResource(id = if (isSpace) CommonStrings.common_no_space_name else CommonStrings.common_no_room_name), + fontStyle = FontStyle.Italic.takeIf { name == null }, + color = ElementTheme.colors.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (showIndicator) { + UnreadIndicatorAtom( + color = ElementTheme.colors.unreadIndicator + ) + } + } +} + +@Composable +private fun SpaceRoomItemScaffold( + avatarData: AvatarData, + isSpace: Boolean, + onClick: () -> Unit, + onLongClick: () -> Unit, + hideAvatars: Boolean, + modifier: Modifier = Modifier, + trailingAction: @Composable (() -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit, +) { + val clickModifier = Modifier + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + indication = ripple(), + interactionSource = remember { MutableInteractionSource() } + ) + .onKeyboardContextMenuAction { onLongClick } + Row( + modifier = modifier + .fillMaxWidth() + .then(clickModifier) + .padding(horizontal = 16.dp, vertical = 8.dp) + .height(IntrinsicSize.Min), + ) { + Avatar( + avatarData = avatarData, + avatarType = if (isSpace) AvatarType.Space() else AvatarType.Room(), + hideImage = hideAvatars, + ) + Spacer(modifier = Modifier.width(16.dp)) + Column( + modifier = Modifier.weight(1f), + content = content, + ) + if (trailingAction != null) { + Spacer(modifier = Modifier.width(16.dp)) + trailingAction() + } + } +} + +@Composable +@ReadOnlyComposable +private fun SpaceRoom.subtitle(): String { + return if (isSpace) { + if (joinRule == JoinRule.Public) { + stringResource(CommonStrings.common_public_space) + } else { + stringResource(CommonStrings.common_private_space) + } + } else { + pluralStringResource(CommonPlurals.common_member_count, numJoinedMembers, numJoinedMembers) + } +} + +@Composable +@ReadOnlyComposable +private fun SpaceRoom.info(): String { + return if (isSpace) { + stringResource( + CommonStrings.screen_space_list_details, + pluralStringResource(CommonPlurals.common_rooms, childrenCount, childrenCount), + pluralStringResource(CommonPlurals.common_member_count, numJoinedMembers, numJoinedMembers), + ) + } else { + topic.orEmpty() + } +} + +@Composable +private fun SpaceRoom.visibilityIcon(): ImageVector? { + return if (joinRule == JoinRule.Public) { + CompoundIcons.Public() + } else { + CompoundIcons.LockSolid() + } +} + +@Composable +@PreviewsDayNight +internal fun SpaceRoomItemViewPreview(@PreviewParameter(SpaceRoomProvider::class) spaceRoom: SpaceRoom) = ElementPreview { + SpaceRoomItemView( + spaceRoom = spaceRoom, + showUnreadIndicator = spaceRoom.state == CurrentUserMembership.INVITED, + hideAvatars = false, + onClick = {}, + onLongClick = {}, + modifier = Modifier.fillMaxWidth(), + bottomAction = if (spaceRoom.state == CurrentUserMembership.INVITED) { + { InviteButtonsRowMolecule({}, {}) } + } else { + null + }, + trailingAction = when (spaceRoom.state) { + null, CurrentUserMembership.LEFT -> { + { + JoinButton( + showProgress = spaceRoom.state == CurrentUserMembership.LEFT, + onClick = { }, + ) + } + } + else -> null + } + ) +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt new file mode 100644 index 0000000000..bfdfeeac59 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.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.libraries.matrix.ui.components + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +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.spaces.SpaceRoom +import io.element.android.libraries.previewutils.room.aSpaceRoom + +class SpaceRoomProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + aSpaceRoom( + roomType = RoomType.Room, + name = "Room name with topic", + topic = "Room topic that is quite long and might be truncated" + ), + aSpaceRoom( + roomType = RoomType.Room, + name = "Room name no topic", + state = CurrentUserMembership.LEFT, + ), + aSpaceRoom( + roomType = RoomType.Room, + name = "Room name with topic", + topic = "Room topic that is quite long and might be truncated", + state = CurrentUserMembership.INVITED, + ), + aSpaceRoom( + roomType = RoomType.Room, + name = "Room name no topic", + state = CurrentUserMembership.INVITED, + ), + aSpaceRoom( + numJoinedMembers = 5, + childrenCount = 10, + worldReadable = true, + roomId = RoomId("!spaceId0:example.com"), + ), + aSpaceRoom( + numJoinedMembers = 5, + childrenCount = 10, + worldReadable = true, + avatarUrl = "anUrl", + roomId = RoomId("!spaceId1:example.com"), + state = CurrentUserMembership.LEFT, + ), + aSpaceRoom( + name = null, + numJoinedMembers = 5, + childrenCount = 10, + worldReadable = true, + avatarUrl = "anUrl", + roomId = RoomId("!spaceId2:example.com"), + state = CurrentUserMembership.INVITED, + ), + aSpaceRoom( + name = null, + numJoinedMembers = 5, + childrenCount = 10, + worldReadable = true, + avatarUrl = "anUrl", + roomId = RoomId("!spaceId3:example.com"), + state = CurrentUserMembership.INVITED, + ), + ) +} 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 f8722a93a4..dc36eb3878 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 @@ -13,20 +13,21 @@ 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 +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.Provider +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClient import okhttp3.OkHttpClient -import javax.inject.Inject -import javax.inject.Provider interface LoggedInImageLoaderFactory { fun newImageLoader(matrixClient: MatrixClient): ImageLoader } @ContributesBinding(AppScope::class) -class DefaultLoggedInImageLoaderFactory @Inject constructor( +@Inject +class DefaultLoggedInImageLoaderFactory( @ApplicationContext private val context: Context, private val okHttpClient: Provider, ) : LoggedInImageLoaderFactory { @@ -37,7 +38,7 @@ class DefaultLoggedInImageLoaderFactory @Inject constructor( OkHttpNetworkFetcherFactory( callFactory = { // Use newBuilder, see https://coil-kt.github.io/coil/network/#using-a-custom-okhttpclient - okHttpClient.get().newBuilder().build() + okHttpClient().newBuilder().build() } ) ) @@ -56,7 +57,8 @@ class DefaultLoggedInImageLoaderFactory @Inject constructor( } } -class NotLoggedInImageLoaderFactory @Inject constructor( +@Inject +class NotLoggedInImageLoaderFactory( @ApplicationContext private val context: Context, private val okHttpClient: Provider, ) { @@ -67,7 +69,7 @@ class NotLoggedInImageLoaderFactory @Inject constructor( OkHttpNetworkFetcherFactory( callFactory = { // Use newBuilder, see https://coil-kt.github.io/coil/network/#using-a-custom-okhttpclient - okHttpClient.get().newBuilder().build() + okHttpClient().newBuilder().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 0c9e0cb938..82080c66cc 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 @@ -8,14 +8,14 @@ package io.element.android.libraries.matrix.ui.media import coil3.ImageLoader -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.libraries.matrix.api.MatrixClient 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 javax.inject.Inject interface ImageLoaderHolder { fun get(client: MatrixClient): ImageLoader @@ -24,7 +24,8 @@ interface ImageLoaderHolder { @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) -class DefaultImageLoaderHolder @Inject constructor( +@Inject +class DefaultImageLoaderHolder( private val loggedInImageLoaderFactory: LoggedInImageLoaderFactory, private val sessionObserver: SessionObserver, ) : ImageLoaderHolder { 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 0c02a24a0e..58dac492dc 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,18 +7,19 @@ package io.element.android.libraries.matrix.ui.messages +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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.drop import kotlinx.coroutines.flow.runningFold -import javax.inject.Inject @SingleIn(RoomScope::class) -class RoomMemberProfilesCache @Inject constructor() { +@Inject +class RoomMemberProfilesCache { private val cache = MutableStateFlow(mapOf()) val updateFlow = cache.drop(1).runningFold(0) { acc, _ -> acc + 1 } 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 index 598b7c28da..f9253d351f 100644 --- 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 @@ -7,8 +7,9 @@ package io.element.android.libraries.matrix.ui.messages +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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 @@ -16,10 +17,10 @@ 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() { +@Inject +class RoomNamesCache { private val cache = MutableStateFlow(mapOf()) val updateFlow = cache.drop(1).runningFold(0) { acc, _ -> acc + 1 } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt index 70803c5653..f43c409f0e 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt @@ -134,10 +134,7 @@ class InReplyToDetailsOtherProvider : InReplyToDetailsProvider() { private fun aMessageContent( body: String, type: MessageType, - threadInfo: EventThreadInfo = EventThreadInfo( - threadRootId = null, - threadSummary = null, - ), + threadInfo: EventThreadInfo? = null, ) = MessageContent( body = body, inReplyTo = null, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SpaceExtension.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SpaceExtension.kt new file mode 100644 index 0000000000..e4b056fdea --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SpaceExtension.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.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.spaces.SpaceRoom + +fun SpaceRoom.getAvatarData(size: AvatarSize) = AvatarData( + id = roomId.value, + name = name, + url = avatarUrl, + size = size, +) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt index 7fc95035c6..ef266f44f2 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.ui.room import androidx.compose.runtime.Immutable import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import dev.zacsweers.metro.Inject 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 @@ -19,7 +20,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject @Immutable sealed interface LoadingRoomState { @@ -36,7 +36,8 @@ open class LoadingRoomStateProvider : PreviewParameterProvider ) } -class LoadingRoomStateFlowFactory @Inject constructor(private val matrixClient: MatrixClient) { +@Inject +class LoadingRoomStateFlowFactory(private val matrixClient: MatrixClient) { fun create(lifecycleScope: CoroutineScope, roomId: RoomId): StateFlow = getJoinedRoomFlow(roomId) .map { room -> diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/safety/Avatars.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/safety/Avatars.kt new file mode 100644 index 0000000000..0eb2b37ea4 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/safety/Avatars.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.matrix.ui.safety + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import io.element.android.libraries.core.coroutine.mapState +import io.element.android.libraries.matrix.api.MatrixClient + +@Composable +fun MatrixClient.rememberHideInvitesAvatar(): State { + return remember { + mediaPreviewService() + .mediaPreviewConfigFlow + .mapState { config -> config.hideInviteAvatar } + }.collectAsState() +} diff --git a/libraries/matrixui/src/main/res/values-de/translations.xml b/libraries/matrixui/src/main/res/values-de/translations.xml index 67d42162c3..d779c15659 100644 --- a/libraries/matrixui/src/main/res/values-de/translations.xml +++ b/libraries/matrixui/src/main/res/values-de/translations.xml @@ -1,7 +1,7 @@ "Einladung senden" - "Möchten Sie einen Chat mit %1$s starten?" + "Möchtest du einen Chat mit %1$s starten?" "Einladung senden?" "%1$s (%2$s) hat dich eingeladen" diff --git a/libraries/matrixui/src/main/res/values-ko/translations.xml b/libraries/matrixui/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..09d831e348 --- /dev/null +++ b/libraries/matrixui/src/main/res/values-ko/translations.xml @@ -0,0 +1,7 @@ + + + "초대장 보내기" + "%1$s 와 채팅을 시작하시겠습니까?" + "초대장을 보내시겠습니까?" + "%1$s (%2$s) 당신을 초대했습니다" + 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 a00e478b92..ed6acf2039 100644 --- a/libraries/matrixui/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/matrixui/src/main/res/values-pt-rBR/translations.xml @@ -1,7 +1,7 @@ "Enviar convite" - "Gostaria de iniciar um bate-papo com %1$s?" + "Gostaria de iniciar uma conversa com %1$s?" "Enviar convite?" "%1$s(%2$s) convidou você" diff --git a/libraries/matrixui/src/main/res/values-ro/translations.xml b/libraries/matrixui/src/main/res/values-ro/translations.xml index 08576cc20e..5156d6bd16 100644 --- a/libraries/matrixui/src/main/res/values-ro/translations.xml +++ b/libraries/matrixui/src/main/res/values-ro/translations.xml @@ -1,4 +1,7 @@ + "Trimiteți invitația" + "Doriți să începeți o discuție cu %1$s?" + "Trimiteți invitația?" "%1$s (%2$s) v-a invitat." 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 5b5df0e9b7..6b9683b0e0 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 @@ -8,7 +8,6 @@ package io.element.android.libraries.matrix.ui.messages.reply import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange @@ -70,7 +69,7 @@ class InReplyToDetailTest { body = "**Hello!**", inReplyTo = null, isEdited = false, - threadInfo = EventThreadInfo(threadRootId = null, threadSummary = null), + threadInfo = null, type = TextMessageType( body = "**Hello!**", formatted = FormattedBody( @@ -95,7 +94,7 @@ class InReplyToDetailTest { body = "**Hello!**", inReplyTo = null, isEdited = false, - threadInfo = EventThreadInfo(threadRootId = null, threadSummary = null), + threadInfo = null, type = TextMessageType( body = "**Hello!**", formatted = null, diff --git a/libraries/mediapickers/api/build.gradle.kts b/libraries/mediapickers/api/build.gradle.kts index f82a09b37a..c130cd7900 100644 --- a/libraries/mediapickers/api/build.gradle.kts +++ b/libraries/mediapickers/api/build.gradle.kts @@ -1,4 +1,4 @@ -import extension.setupAnvil +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -11,8 +11,6 @@ plugins { id("io.element.android-compose-library") } -setupAnvil() - android { namespace = "io.element.android.libraries.mediapickers.api" @@ -20,11 +18,7 @@ android { implementation(projects.libraries.uiStrings) implementation(projects.libraries.core) implementation(projects.libraries.di) - implementation(libs.inject) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.truth) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs) } } diff --git a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt index 961f486322..4a2f26a4b3 100644 --- a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt +++ b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt @@ -25,7 +25,7 @@ interface PickerProvider { @Composable fun registerFilePicker( mimeType: String, - onResult: (Uri?) -> Unit + onResult: (uri: Uri?, mimeType: String?) -> Unit, ): PickerLauncher @Composable diff --git a/libraries/mediapickers/impl/build.gradle.kts b/libraries/mediapickers/impl/build.gradle.kts index c21d096e92..6a90b95306 100644 --- a/libraries/mediapickers/impl/build.gradle.kts +++ b/libraries/mediapickers/impl/build.gradle.kts @@ -1,4 +1,4 @@ -import extension.setupAnvil +import extension.setupDependencyInjection /* * Copyright 2023, 2024 New Vector Ltd. @@ -11,7 +11,7 @@ plugins { id("io.element.android-compose-library") } -setupAnvil() +setupDependencyInjection() android { namespace = "io.element.android.libraries.mediapickers.impl" @@ -20,6 +20,5 @@ android { dependencies { implementation(projects.libraries.core) implementation(projects.libraries.di) - implementation(libs.inject) api(projects.libraries.mediapickers.api) } diff --git a/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/DefaultPickerProvider.kt b/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/DefaultPickerProvider.kt index add63d4a32..878c9ea865 100644 --- a/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/DefaultPickerProvider.kt +++ b/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/DefaultPickerProvider.kt @@ -15,19 +15,20 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalInspectionMode import androidx.core.content.FileProvider -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.mediapickers.api.ComposePickerLauncher import io.element.android.libraries.mediapickers.api.NoOpPickerLauncher import io.element.android.libraries.mediapickers.api.PickerLauncher import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.api.PickerType import java.io.File -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultPickerProvider @Inject constructor( +@Inject +class DefaultPickerProvider( @ApplicationContext private val context: Context, ) : PickerProvider { /** @@ -87,13 +88,16 @@ class DefaultPickerProvider @Inject constructor( @Composable override fun registerFilePicker( mimeType: String, - onResult: (Uri?) -> Unit, + onResult: (uri: Uri?, mimeType: String?) -> Unit, ): PickerLauncher { // Tests and UI preview can't handle Context or FileProviders, so we might as well disable the whole picker return if (LocalInspectionMode.current) { - NoOpPickerLauncher { onResult(null) } + NoOpPickerLauncher { onResult(null, null) } } else { - rememberPickerLauncher(type = PickerType.File(mimeType)) { uri -> onResult(uri) } + rememberPickerLauncher(type = PickerType.File(mimeType)) { uri -> + val pickedMimeType = uri?.let { context.contentResolver.getType(it) } + onResult(uri, pickedMimeType) + } } } diff --git a/libraries/mediapickers/test/build.gradle.kts b/libraries/mediapickers/test/build.gradle.kts index 4b10dca551..ee743bb63d 100644 --- a/libraries/mediapickers/test/build.gradle.kts +++ b/libraries/mediapickers/test/build.gradle.kts @@ -1,4 +1,4 @@ -import extension.setupAnvil +import extension.setupDependencyInjection /* * Copyright 2023, 2024 New Vector Ltd. @@ -11,7 +11,7 @@ plugins { id("io.element.android-compose-library") } -setupAnvil() +setupDependencyInjection() android { namespace = "io.element.android.libraries.mediapickers.test" @@ -19,7 +19,6 @@ android { dependencies { implementation(projects.libraries.core) implementation(projects.libraries.di) - implementation(libs.inject) api(projects.libraries.mediapickers.api) } } diff --git a/libraries/mediapickers/test/src/main/kotlin/io/element/android/libraries/mediapickers/test/FakePickerProvider.kt b/libraries/mediapickers/test/src/main/kotlin/io/element/android/libraries/mediapickers/test/FakePickerProvider.kt index 8dfd88dcd6..3a279868bf 100644 --- a/libraries/mediapickers/test/src/main/kotlin/io/element/android/libraries/mediapickers/test/FakePickerProvider.kt +++ b/libraries/mediapickers/test/src/main/kotlin/io/element/android/libraries/mediapickers/test/FakePickerProvider.kt @@ -30,8 +30,8 @@ class FakePickerProvider : PickerProvider { } @Composable - override fun registerFilePicker(mimeType: String, onResult: (Uri?) -> Unit): PickerLauncher { - return NoOpPickerLauncher { onResult(result) } + override fun registerFilePicker(mimeType: String, onResult: (Uri?, String?) -> Unit): PickerLauncher { + return NoOpPickerLauncher { onResult(result, this.mimeType) } } @Composable diff --git a/libraries/mediaplayer/api/build.gradle.kts b/libraries/mediaplayer/api/build.gradle.kts index 0e47c1be25..f096738c6f 100644 --- a/libraries/mediaplayer/api/build.gradle.kts +++ b/libraries/mediaplayer/api/build.gradle.kts @@ -1,5 +1,3 @@ -import extension.setupAnvil - /* * Copyright 2023, 2024 New Vector Ltd. * @@ -14,8 +12,6 @@ android { namespace = "io.element.android.libraries.mediaplayer.api" } -setupAnvil() - dependencies { implementation(projects.libraries.matrix.api) implementation(libs.coroutines.core) diff --git a/libraries/mediaplayer/impl/build.gradle.kts b/libraries/mediaplayer/impl/build.gradle.kts index 6bd19680a3..bd6382f86f 100644 --- a/libraries/mediaplayer/impl/build.gradle.kts +++ b/libraries/mediaplayer/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -14,25 +15,19 @@ android { namespace = "io.element.android.libraries.mediaplayer.impl" } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.libraries.mediaplayer.api) 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) + testCommonDependencies(libs) testImplementation(projects.libraries.audio.test) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) - testImplementation(libs.test.mockk) - testImplementation(libs.test.turbine) testImplementation(libs.coroutines.core) - testImplementation(libs.coroutines.test) } 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 45e600f8b9..5aad37779c 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 @@ -10,11 +10,12 @@ package io.element.android.libraries.mediaplayer.impl import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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 @@ -28,7 +29,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.timeout import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject import kotlin.time.Duration.Companion.seconds /** @@ -36,7 +36,8 @@ import kotlin.time.Duration.Companion.seconds */ @ContributesBinding(RoomScope::class) @SingleIn(RoomScope::class) -class DefaultMediaPlayer @Inject constructor( +@Inject +class DefaultMediaPlayer( private val player: SimplePlayer, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, 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 df1413161e..c8e42b8abf 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 @@ -11,11 +11,11 @@ import android.content.Context import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides -import io.element.android.libraries.di.ApplicationContext +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.annotations.ApplicationContext /** * A subset of media3 [Player] that only exposes the few methods we need making it easier to mock. @@ -42,7 +42,7 @@ interface SimplePlayer { } @ContributesTo(RoomScope::class) -@Module +@BindingContainer object SimplePlayerModule { @Provides fun simplePlayerProvider( diff --git a/libraries/mediaupload/api/build.gradle.kts b/libraries/mediaupload/api/build.gradle.kts index ce4f8c0f60..bea5398372 100644 --- a/libraries/mediaupload/api/build.gradle.kts +++ b/libraries/mediaupload/api/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -11,7 +12,7 @@ plugins { id("io.element.android-library") } -setupAnvil() +setupDependencyInjection() android { namespace = "io.element.android.libraries.mediaupload.api" @@ -24,15 +25,10 @@ dependencies { implementation(projects.libraries.di) api(projects.libraries.matrix.api) api(projects.libraries.preferences.api) - implementation(libs.inject) implementation(libs.coroutines.core) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.mediaupload.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.robolectric) } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MaxUploadSizeProvider.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MaxUploadSizeProvider.kt index 884d75e325..743a684b11 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MaxUploadSizeProvider.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MaxUploadSizeProvider.kt @@ -7,13 +7,14 @@ package io.element.android.libraries.mediaupload.api +import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.MatrixClient -import javax.inject.Inject /** * Provides the maximum upload size allowed by the Matrix server. */ -class MaxUploadSizeProvider @Inject constructor( +@Inject +class MaxUploadSizeProvider( private val matrixClient: MatrixClient, ) { suspend fun getMaxUploadSize(): Result { 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 e11ec804cd..7592f46fa6 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 @@ -8,9 +8,10 @@ package io.element.android.libraries.mediaupload.api import android.net.Uri -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import io.element.android.libraries.androidutils.hash.hash import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.core.extensions.flatMapCatching import io.element.android.libraries.matrix.api.core.EventId @@ -20,9 +21,12 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.Timeline import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job +import timber.log.Timber +import java.io.File import java.util.concurrent.ConcurrentHashMap -class MediaSender @AssistedInject constructor( +@AssistedInject +class MediaSender( private val preProcessor: MediaPreProcessor, private val room: JoinedRoom, @Assisted private val timelineMode: Timeline.Mode, @@ -43,6 +47,7 @@ class MediaSender @AssistedInject constructor( mimeType: String, mediaOptimizationConfig: MediaOptimizationConfig, ): Result { + Timber.d("Pre-processing media | uri: ${mediaId(uri)} | mimeType: $mimeType") return preProcessor .process( uri = uri, @@ -58,7 +63,9 @@ class MediaSender @AssistedInject constructor( formattedCaption: String?, inReplyToEventId: EventId?, ): Result { + val mediaLogId = mediaId(mediaUploadInfo.file) return getTimeline().flatMap { + Timber.d("Started sending media $mediaLogId using timeline: ${it.mode}") it.sendMedia( uploadInfo = mediaUploadInfo, caption = caption, @@ -66,7 +73,7 @@ class MediaSender @AssistedInject constructor( inReplyToEventId = inReplyToEventId, ) } - .handleSendResult() + .handleSendResult(mediaLogId) } suspend fun sendMedia( @@ -92,7 +99,7 @@ class MediaSender @AssistedInject constructor( inReplyToEventId = inReplyToEventId, ) } - .handleSendResult() + .handleSendResult(mediaId(uri)) } suspend fun sendVoiceMessage( @@ -122,17 +129,19 @@ class MediaSender @AssistedInject constructor( inReplyToEventId = inReplyToEventId, ) } - .handleSendResult() + .handleSendResult(mediaId(uri)) } - private fun Result.handleSendResult() = this + private fun Result.handleSendResult(mediaId: String) = this .onFailure { error -> val job = ongoingUploadJobs.remove(Job) + Timber.e(error, "Sending media $mediaId failed. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}") if (error !is CancellationException) { job?.cancel() } } .onSuccess { + Timber.d("Sent media $mediaId successfully. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}") ongoingUploadJobs.remove(Job) } @@ -195,6 +204,7 @@ class MediaSender @AssistedInject constructor( @Suppress("RunCatchingNotAllowed") return handler .mapCatching { uploadHandler -> + Timber.d("Added ongoing upload job, total: ${ongoingUploadJobs.size + 1}") ongoingUploadJobs[Job] = uploadHandler uploadHandler.await() } @@ -214,3 +224,6 @@ class MediaSender @AssistedInject constructor( */ fun cleanUp() = preProcessor.cleanUp() } + +private fun mediaId(uri: Uri?): String = uri?.path.orEmpty().hash() +private fun mediaId(file: File): String = file.path.orEmpty().hash() diff --git a/libraries/mediaupload/impl/build.gradle.kts b/libraries/mediaupload/impl/build.gradle.kts index be2637e6e1..ffd01aac2f 100644 --- a/libraries/mediaupload/impl/build.gradle.kts +++ b/libraries/mediaupload/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.libraries.mediaupload.api) @@ -31,7 +32,6 @@ dependencies { implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) implementation(projects.services.toolbox.api) - implementation(libs.inject) implementation(libs.androidx.exifinterface) implementation(libs.androidx.media3.transformer) implementation(libs.androidx.media3.effect) @@ -39,10 +39,6 @@ dependencies { implementation(libs.coroutines.core) implementation(libs.vanniktech.blurhash) - testImplementation(libs.test.junit) - testImplementation(libs.test.robolectric) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.truth) - testImplementation(projects.tests.testutils) + testCommonDependencies(libs) testImplementation(projects.services.toolbox.test) } 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 e1758e9429..73c6e9c64a 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 @@ -12,11 +12,14 @@ import android.graphics.BitmapFactory import android.media.MediaMetadataRetriever import android.net.Uri import androidx.exifinterface.media.ExifInterface -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.file.TemporaryUriDeleter import io.element.android.libraries.androidutils.file.createTmpFile import io.element.android.libraries.androidutils.file.getFileName import io.element.android.libraries.androidutils.file.safeRenameTo +import io.element.android.libraries.androidutils.hash.hash import io.element.android.libraries.androidutils.media.runAndRelease import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.data.tryOrNull @@ -26,8 +29,7 @@ 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 import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.ApplicationContext 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 @@ -44,12 +46,12 @@ import timber.log.Timber import java.io.File import java.io.InputStream import java.util.UUID -import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @ContributesBinding(AppScope::class) -class AndroidMediaPreProcessor @Inject constructor( +@Inject +class AndroidMediaPreProcessor( @ApplicationContext private val context: Context, private val thumbnailFactory: ThumbnailFactory, private val imageCompressor: ImageCompressor, @@ -107,6 +109,8 @@ class AndroidMediaPreProcessor @Inject constructor( }.mapFailure { MediaPreProcessor.Failure(it) } override fun cleanUp() { + Timber.d("Cleaning up temporary media files") + // Clear temporary files created in older versions of the app cacheDir.listFiles()?.onEach { file -> if (file.isFile) { @@ -129,6 +133,7 @@ class AndroidMediaPreProcessor @Inject constructor( } private suspend fun processFile(uri: Uri, mimeType: String): MediaUploadInfo { + Timber.d("Processing file ${uri.path.orEmpty().hash()}") val file = copyToTmpFile(uri) val info = FileInfo( mimetype = mimeType, @@ -140,6 +145,7 @@ class AndroidMediaPreProcessor @Inject constructor( } private fun MediaUploadInfo.postProcess(uri: Uri): MediaUploadInfo { + Timber.d("Finished processing, post-processing ${uri.path.orEmpty().hash()}") val name = context.getFileName(uri) ?: return this val renamedFile = File(context.cacheDir, name).also { file.safeRenameTo(it) @@ -154,6 +160,7 @@ class AndroidMediaPreProcessor @Inject constructor( } private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo { + Timber.d("Processing image ${uri.path.orEmpty().hash()}") suspend fun processImageWithCompression(): MediaUploadInfo { // Read the orientation metadata from its own stream. Trying to reuse this stream for compression will fail. val orientation = contentResolver.openInputStream(uri).use { input -> @@ -217,6 +224,7 @@ class AndroidMediaPreProcessor @Inject constructor( } private suspend fun processVideo(uri: Uri, mimeType: String?, videoCompressionPreset: VideoCompressionPreset): MediaUploadInfo { + Timber.d("Processing video ${uri.path.orEmpty().hash()}") val resultFile = runCatchingExceptions { videoCompressor.compress(uri, videoCompressionPreset) .onEach { @@ -244,12 +252,14 @@ class AndroidMediaPreProcessor @Inject constructor( thumbnailFile = thumbnailInfo?.file ) } else { + Timber.d("Could not transcode video ${uri.path.orEmpty().hash()}, sending original file as plain file") // 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 { + Timber.d("Processing audio ${uri.path.orEmpty().hash()}") val file = copyToTmpFile(uri) return MediaMetadataRetriever().runAndRelease { setDataSource(context, Uri.fromFile(file)) diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/DefaultMediaOptimizationConfigProvider.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/DefaultMediaOptimizationConfigProvider.kt index 7642599077..f3e89730bd 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/DefaultMediaOptimizationConfigProvider.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/DefaultMediaOptimizationConfigProvider.kt @@ -7,16 +7,17 @@ package io.element.android.libraries.mediaupload.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.di.SessionScope import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import kotlinx.coroutines.flow.first -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultMediaOptimizationConfigProvider @Inject constructor( +@Inject +class DefaultMediaOptimizationConfigProvider( private val sessionPreferencesStore: SessionPreferencesStore, ) : MediaOptimizationConfigProvider { override suspend fun get(): MediaOptimizationConfig = MediaOptimizationConfig( 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 1076773873..3bcb367575 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 @@ -11,19 +11,20 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import androidx.exifinterface.media.ExifInterface +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.bitmap.calculateInSampleSize import io.element.android.libraries.androidutils.bitmap.resizeToMax import io.element.android.libraries.androidutils.bitmap.rotateToExifMetadataOrientation 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 io.element.android.libraries.di.annotations.ApplicationContext import kotlinx.coroutines.withContext import java.io.File import java.io.InputStream -import javax.inject.Inject -class ImageCompressor @Inject constructor( +@Inject +class ImageCompressor( @ApplicationContext private val context: Context, private val dispatchers: CoroutineDispatchers, ) { diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ThumbnailFactory.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ThumbnailFactory.kt index 7151836f17..c1c90302ba 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ThumbnailFactory.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ThumbnailFactory.kt @@ -19,18 +19,18 @@ import android.provider.MediaStore import android.util.Size import androidx.core.net.toUri import com.vanniktech.blurhash.BlurHash +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.bitmap.resizeToMax import io.element.android.libraries.androidutils.file.createTmpFile import io.element.android.libraries.androidutils.media.runAndRelease import io.element.android.libraries.core.mimetype.MimeTypes -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.media.ThumbnailInfo import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import kotlinx.coroutines.suspendCancellableCoroutine import timber.log.Timber import java.io.File import java.io.IOException -import javax.inject.Inject import kotlin.coroutines.resume /** @@ -50,7 +50,8 @@ private const val THUMB_MAX_HEIGHT = 600 */ private const val VIDEO_THUMB_FRAME = 0L -class ThumbnailFactory @Inject constructor( +@Inject +class ThumbnailFactory( @ApplicationContext private val context: Context, private val sdkIntProvider: BuildVersionSdkIntProvider ) { @@ -123,8 +124,8 @@ class ThumbnailFactory @Inject constructor( val thumbnailResult = ThumbnailResult( file = thumbnailFile, info = ThumbnailInfo( - height = bitmapThumbnail.height.toLong(), width = bitmapThumbnail.width.toLong(), + height = bitmapThumbnail.height.toLong(), mimetype = mimeTypeToThumbnailMimeType(mimeType), size = thumbnailFile.length() ), 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 c21bd301ef..3c289ad942 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 @@ -27,10 +27,11 @@ import androidx.media3.transformer.ProgressHolder import androidx.media3.transformer.TransformationRequest import androidx.media3.transformer.Transformer import androidx.media3.transformer.VideoEncoderSettings +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.file.createTmpFile import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.core.extensions.runCatchingExceptions -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.preferences.api.store.VideoCompressionPreset import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose @@ -42,9 +43,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File -import javax.inject.Inject -class VideoCompressor @Inject constructor( +@Inject +class VideoCompressor( @ApplicationContext private val context: Context, ) { @OptIn(UnstableApi::class) @@ -61,8 +62,8 @@ class VideoCompressor @Inject constructor( val width = metadata?.width ?: Int.MAX_VALUE val height = metadata?.height ?: Int.MAX_VALUE - val videoResizeEffect = videoCompressorConfig.videoCompressorHelper?.let { - val outputSize = it.getOutputSize(Size(width, height)) + val videoResizeEffect = run { + val outputSize = videoCompressorConfig.videoCompressorHelper.getOutputSize(Size(width, height)) if (metadata?.rotation == 90 || metadata?.rotation == 270) { // If the video is rotated, we need to swap width and height Presentation.createForWidthAndHeight( @@ -89,19 +90,14 @@ class VideoCompressor @Inject constructor( val inputMediaItem = MediaItem.fromUri(uri) val outputMediaItem = EditedMediaItem.Builder(inputMediaItem) .setFrameRate(newFrameRate) - .run { - if (videoResizeEffect != null) { - setEffects(Effects(emptyList(), listOf(videoResizeEffect))) - } else { - this - } - } + .setEffects(Effects(emptyList(), listOf(videoResizeEffect))) .build() val encoderFactory = DefaultEncoderFactory.Builder(context) .setRequestedVideoEncoderSettings( VideoEncoderSettings.Builder() - .setBitrateMode(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR) + // Use VBR which is generally better for quality and compatibility, although slightly worse for file size + .setBitrateMode(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR) .setBitrate(newBitrate) .build() ) diff --git a/libraries/mediaviewer/api/build.gradle.kts b/libraries/mediaviewer/api/build.gradle.kts index 1a0ce019c7..0f8aad9561 100644 --- a/libraries/mediaviewer/api/build.gradle.kts +++ b/libraries/mediaviewer/api/build.gradle.kts @@ -1,5 +1,3 @@ -import extension.setupAnvil - /* * Copyright 2023, 2024 New Vector Ltd. * @@ -15,8 +13,6 @@ android { namespace = "io.element.android.libraries.mediaviewer.api" } -setupAnvil() - dependencies { implementation(projects.libraries.core) implementation(projects.libraries.architecture) diff --git a/libraries/mediaviewer/impl/build.gradle.kts b/libraries/mediaviewer/impl/build.gradle.kts index 311a0441c8..4af1c54f46 100644 --- a/libraries/mediaviewer/impl/build.gradle.kts +++ b/libraries/mediaviewer/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -20,12 +21,10 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(libs.coroutines.core) - implementation(libs.dagger) - implementation(libs.coil.compose) implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.ui) @@ -54,21 +53,12 @@ dependencies { implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) + testCommonDependencies(libs, true) + testImplementation(projects.libraries.audio.test) testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.mediaviewer.test) testImplementation(projects.services.toolbox.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) - testImplementation(libs.test.mockk) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.turbine) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.turbine) testImplementation(libs.coroutines.core) - testImplementation(libs.coroutines.test) - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPoint.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPoint.kt index 7d4a577604..0b6c2cc6cb 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPoint.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPoint.kt @@ -10,15 +10,16 @@ package io.element.android.libraries.mediaviewer.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint -import io.element.android.libraries.mediaviewer.impl.gallery.root.MediaGalleryRootNode -import javax.inject.Inject +import io.element.android.libraries.mediaviewer.impl.gallery.root.MediaGalleryFlowNode @ContributesBinding(AppScope::class) -class DefaultMediaGalleryEntryPoint @Inject constructor() : MediaGalleryEntryPoint { +@Inject +class DefaultMediaGalleryEntryPoint : MediaGalleryEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MediaGalleryEntryPoint.NodeBuilder { val plugins = ArrayList() @@ -29,7 +30,7 @@ class DefaultMediaGalleryEntryPoint @Inject constructor() : MediaGalleryEntryPoi } override fun build(): Node { - return parentNode.createNode(buildContext, plugins) + return parentNode.createNode(buildContext, plugins) } } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt index 2a137bb387..d2800ef9b8 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt @@ -10,19 +10,20 @@ package io.element.android.libraries.mediaviewer.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.createNode import io.element.android.libraries.core.mimetype.MimeTypes -import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.impl.viewer.MediaViewerNode -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint { +@Inject +class DefaultMediaViewerEntryPoint : MediaViewerEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MediaViewerEntryPoint.NodeBuilder { val plugins = ArrayList() diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt index ed39faf080..d6d9128869 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.mediaviewer.impl.datasource +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode @@ -42,9 +43,9 @@ import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor import io.element.android.libraries.mediaviewer.impl.model.MediaItem import timber.log.Timber -import javax.inject.Inject -class EventItemFactory @Inject constructor( +@Inject +class EventItemFactory( private val fileSizeFormatter: FileSizeFormatter, private val fileExtensionExtractor: FileExtensionExtractor, private val dateFormatter: DateFormatter, 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 bdbffbfd1a..5d998d25fd 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 @@ -7,14 +7,14 @@ package io.element.android.libraries.mediaviewer.impl.datasource -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.mediaviewer.impl.model.MediaItem -import javax.inject.Inject -interface FocusedTimelineMediaGalleryDataSourceFactory { +fun interface FocusedTimelineMediaGalleryDataSourceFactory { fun createFor( eventId: EventId, mediaItem: MediaItem.Event, @@ -23,7 +23,8 @@ interface FocusedTimelineMediaGalleryDataSourceFactory { } @ContributesBinding(RoomScope::class) -class DefaultFocusedTimelineMediaGalleryDataSourceFactory @Inject constructor( +@Inject +class DefaultFocusedTimelineMediaGalleryDataSourceFactory( private val room: JoinedRoom, private val timelineMediaItemsFactory: TimelineMediaItemsFactory, private val 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 a7ee01aa57..f40bb08a86 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 @@ -7,10 +7,11 @@ package io.element.android.libraries.mediaviewer.impl.datasource -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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.BaseRoom import io.element.android.libraries.matrix.api.timeline.Timeline @@ -27,7 +28,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import java.util.concurrent.atomic.AtomicBoolean -import javax.inject.Inject interface MediaGalleryDataSource { fun start() @@ -39,7 +39,8 @@ interface MediaGalleryDataSource { @SingleIn(RoomScope::class) @ContributesBinding(RoomScope::class) -class TimelineMediaGalleryDataSource @Inject constructor( +@Inject +class TimelineMediaGalleryDataSource( private val room: BaseRoom, private val mediaTimeline: MediaTimeline, private val timelineMediaItemsFactory: TimelineMediaItemsFactory, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaItemsPostProcessor.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaItemsPostProcessor.kt index 1d3fd09a6c..102fc611ef 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaItemsPostProcessor.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaItemsPostProcessor.kt @@ -7,12 +7,13 @@ package io.element.android.libraries.mediaviewer.impl.datasource +import dev.zacsweers.metro.Inject import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems import io.element.android.libraries.mediaviewer.impl.model.MediaItem import kotlinx.collections.immutable.toImmutableList -import javax.inject.Inject -class MediaItemsPostProcessor @Inject constructor() { +@Inject +class MediaItemsPostProcessor { fun process( mediaItems: List, ): GroupedMediaItems { 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 c014689003..c3543ba129 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 @@ -7,9 +7,10 @@ package io.element.android.libraries.mediaviewer.impl.datasource -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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.CreateTimelineParams @@ -21,7 +22,6 @@ import io.element.android.libraries.mediaviewer.impl.model.hasEvent import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import javax.inject.Inject interface MediaTimeline { suspend fun getTimeline(): Result @@ -36,7 +36,8 @@ interface MediaTimeline { */ @SingleIn(RoomScope::class) @ContributesBinding(RoomScope::class) -class LiveMediaTimeline @Inject constructor( +@Inject +class LiveMediaTimeline( private val room: JoinedRoom, ) : MediaTimeline { private var timeline: Timeline? = null diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaItemsFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaItemsFactory.kt index a759ba4d76..f4c8a83a94 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaItemsFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaItemsFactory.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.mediaviewer.impl.datasource +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.diff.DefaultDiffCacheInvalidator import io.element.android.libraries.androidutils.diff.DiffCacheUpdater import io.element.android.libraries.androidutils.diff.MutableListDiffCache @@ -21,9 +22,9 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import javax.inject.Inject -class TimelineMediaItemsFactory @Inject constructor( +@Inject +class TimelineMediaItemsFactory( private val dispatchers: CoroutineDispatchers, private val virtualItemFactory: VirtualItemFactory, private val eventItemFactory: EventItemFactory, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/VirtualItemFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/VirtualItemFactory.kt index 0c45edcf37..125981d4f8 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/VirtualItemFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/VirtualItemFactory.kt @@ -7,14 +7,15 @@ package io.element.android.libraries.mediaviewer.impl.datasource +import dev.zacsweers.metro.Inject import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.mediaviewer.impl.model.MediaItem -import javax.inject.Inject -class VirtualItemFactory @Inject constructor( +@Inject +class VirtualItemFactory( private val dateFormatter: DateFormatter, ) { fun create(timelineItem: MatrixTimelineItem.Virtual): MediaItem? { diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt index 77520763f5..06a3c6a58f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt @@ -14,9 +14,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.mediaviewer.impl.gallery.di.LocalMediaItemPresenterFactories @@ -24,7 +24,8 @@ import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemPresent import io.element.android.libraries.mediaviewer.impl.model.MediaItem @ContributesNode(RoomScope::class) -class MediaGalleryNode @AssistedInject constructor( +@AssistedInject +class MediaGalleryNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: MediaGalleryPresenter.Factory, 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 27fcf2ce3e..ba3d4d5e1f 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 @@ -16,9 +16,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.androidutils.R import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -44,7 +44,8 @@ import io.element.android.libraries.mediaviewer.impl.model.mediaSource import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.launch -class MediaGalleryPresenter @AssistedInject constructor( +@AssistedInject +class MediaGalleryPresenter( @Assisted private val navigator: MediaGalleryNavigator, private val room: BaseRoom, private val mediaGalleryDataSource: MediaGalleryDataSource, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt index 7a2094d890..fe3a65ff4a 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.voiceplayer.api.aVoiceMessageState fun aFakeMediaItemPresenterFactories() = MediaItemPresenterFactories( mapOf( Pair( - MediaItem.Voice::class.java, + MediaItem.Voice::class, MediaItemPresenterFactory { Presenter { aVoiceMessageState() } }, ), ) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt index ad2e49f16f..eb04d88787 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt @@ -7,13 +7,13 @@ package io.element.android.libraries.mediaviewer.impl.gallery.di -import dagger.MapKey +import dev.zacsweers.metro.MapKey import io.element.android.libraries.mediaviewer.impl.model.MediaItem import kotlin.reflect.KClass /** * Annotation to add a factory of type [MediaItemPresenterFactory] to a - * Dagger map multi binding keyed with a subclass of [MediaItem.Event]. + * DI map multi binding keyed with a subclass of [MediaItem.Event]. */ @Retention(AnnotationRetention.RUNTIME) @MapKey diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt index 02c0441d18..eaf0f5a49e 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt @@ -9,25 +9,26 @@ package io.element.android.libraries.mediaviewer.impl.gallery.di import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.multibindings.Multibinds +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.Multibinds +import dev.zacsweers.metro.SingleIn import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.di.SingleIn import io.element.android.libraries.mediaviewer.impl.model.MediaItem -import javax.inject.Inject +import kotlin.reflect.KClass /** - * Dagger module that declares the [MediaItemPresenterFactory] map multi binding. + * Container that declares the [MediaItemPresenterFactory] map multi binding. * * Its sole purpose is to support the case of an empty map multibinding. */ -@Module +@BindingContainer @ContributesTo(RoomScope::class) interface MediaItemPresenterFactoriesModule { @Multibinds - fun multiBindMediaItemPresenterFactories(): @JvmSuppressWildcards Map, MediaItemPresenterFactory<*, *>> + fun multiBindMediaItemPresenterFactories(): @JvmSuppressWildcards Map, MediaItemPresenterFactory<*, *>> } /** @@ -38,8 +39,9 @@ interface MediaItemPresenterFactoriesModule { * goes out of the [LazyColumn] viewport. */ @SingleIn(RoomScope::class) -class MediaItemPresenterFactories @Inject constructor( - private val factories: @JvmSuppressWildcards Map, MediaItemPresenterFactory<*, *>>, +@Inject +class MediaItemPresenterFactories( + private val factories: @JvmSuppressWildcards Map, MediaItemPresenterFactory<*, *>>, ) { private val presenters: MutableMap> = mutableMapOf() @@ -57,7 +59,7 @@ class MediaItemPresenterFactories @Inject constructor( @Composable fun rememberPresenter( content: C, - contentClass: Class, + contentClass: KClass, ): Presenter = remember(content) { presenters[content]?.let { @Suppress("UNCHECKED_CAST") @@ -86,5 +88,5 @@ inline fun MediaItemPresenterFactories.re content: C ): Presenter = rememberPresenter( content = content, - contentClass = C::class.java + contentClass = C::class ) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt index 9730fb4c05..0b3c77eda1 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt @@ -13,7 +13,7 @@ import io.element.android.libraries.mediaviewer.impl.model.MediaItem /** * A factory for a [Presenter] associated with a timeline item. * - * Implementations should be annotated with [AssistedFactory] to be created by Dagger. + * Implementations should be annotated with [dev.zacsweers.metro.AssistedFactory] to be created. * * @param C The timeline item's [MediaItem.Event] subtype. * @param S The [Presenter]'s state class. 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/MediaGalleryFlowNode.kt similarity index 93% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryRootNode.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryFlowNode.kt index 5088616a18..e617829ad0 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/MediaGalleryFlowNode.kt @@ -15,9 +15,9 @@ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode @@ -40,11 +40,12 @@ import io.element.android.libraries.mediaviewer.impl.model.thumbnailSource import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) -class MediaGalleryRootNode @AssistedInject constructor( +@AssistedInject +class MediaGalleryFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val mediaViewerEntryPoint: MediaViewerEntryPoint -) : BaseFlowNode( +) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap, @@ -86,11 +87,11 @@ class MediaGalleryRootNode @AssistedInject constructor( NavTarget.Root -> { val callback = object : MediaGalleryNode.Callback { override fun onBackClick() { - this@MediaGalleryRootNode.onBackClick() + this@MediaGalleryFlowNode.onBackClick() } override fun onViewInTimeline(eventId: EventId) { - this@MediaGalleryRootNode.onViewInTimeline(eventId) + this@MediaGalleryFlowNode.onViewInTimeline(eventId) } override fun onItemClick(item: MediaItem.Event) { @@ -121,7 +122,7 @@ class MediaGalleryRootNode @AssistedInject constructor( } override fun onViewInTimeline(eventId: EventId) { - this@MediaGalleryRootNode.onViewInTimeline(eventId) + this@MediaGalleryFlowNode.onViewInTimeline(eventId) } } mediaViewerEntryPoint.nodeBuilder(this, buildContext) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt index 92acc754fc..830a405110 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt @@ -8,13 +8,13 @@ package io.element.android.libraries.mediaviewer.impl.gallery.voice import androidx.compose.runtime.Composable -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.multibindings.IntoMap +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.IntoMap import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemEventContentKey @@ -24,7 +24,7 @@ import io.element.android.libraries.voiceplayer.api.VoiceMessagePresenterFactory import io.element.android.libraries.voiceplayer.api.VoiceMessageState import kotlin.time.Duration -@Module +@BindingContainer @ContributesTo(RoomScope::class) interface VoiceMessagePresenterModule { @Binds @@ -33,7 +33,8 @@ interface VoiceMessagePresenterModule { fun bindVoiceMessagePresenterFactory(factory: VoiceMessagePresenter.Factory): MediaItemPresenterFactory<*, *> } -class VoiceMessagePresenter @AssistedInject constructor( +@AssistedInject +class VoiceMessagePresenter( voiceMessagePresenterFactory: VoiceMessagePresenterFactory, @Assisted private val item: MediaItem.Voice, ) : Presenter { 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 d6c43ce4c4..6e7508a947 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 @@ -29,14 +29,15 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.mediaviewer.api.local.LocalMedia import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -44,10 +45,10 @@ import timber.log.Timber import java.io.File import java.io.FileOutputStream import java.io.InputStream -import javax.inject.Inject @ContributesBinding(AppScope::class) -class AndroidLocalMediaActions @Inject constructor( +@Inject +class AndroidLocalMediaActions( @ApplicationContext private val context: Context, private val coroutineDispatchers: CoroutineDispatchers, private val buildMeta: BuildMeta, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt index aa4b2ec53e..4854c1e2f9 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt @@ -10,14 +10,15 @@ package io.element.android.libraries.mediaviewer.impl.local import android.content.Context import android.net.Uri import androidx.core.net.toUri -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.file.getFileName import io.element.android.libraries.androidutils.file.getFileSize import io.element.android.libraries.androidutils.file.getMimeType import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.core.mimetype.MimeTypes -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.toFile @@ -25,10 +26,10 @@ import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor -import javax.inject.Inject @ContributesBinding(AppScope::class) -class AndroidLocalMediaFactory @Inject constructor( +@Inject +class AndroidLocalMediaFactory( @ApplicationContext private val context: Context, private val fileSizeFormatter: FileSizeFormatter, private val fileExtensionExtractor: FileExtensionExtractor, 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 41c292a921..1e69abecc1 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 @@ -10,19 +10,20 @@ package io.element.android.libraries.mediaviewer.impl.local import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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( +@Inject +class DefaultLocalMediaRenderer( private val textFileViewer: TextFileViewer, private val audioFocus: AudioFocus, ) : LocalMediaRenderer { 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 8e826383b6..0ce19aaa84 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 @@ -10,12 +10,14 @@ package io.element.android.libraries.mediaviewer.impl.local.video import android.annotation.SuppressLint import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout +import androidx.annotation.OptIn import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -33,6 +35,7 @@ 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.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView @@ -55,6 +58,7 @@ 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.zoomable +import timber.log.Timber import kotlin.time.Duration.Companion.seconds @SuppressLint("UnsafeOptInUsageError") @@ -165,35 +169,6 @@ private fun ExoPlayerMediaVideoView( } } - LaunchedEffect(exoPlayer.isPlaying) { - if (exoPlayer.isPlaying) { - while (true) { - mediaPlayerControllerState = mediaPlayerControllerState.copy( - progressInMillis = exoPlayer.currentPosition, - ) - delay(200) - } - } else { - // Ensure we render the final state - mediaPlayerControllerState = mediaPlayerControllerState.copy( - progressInMillis = exoPlayer.currentPosition, - ) - } - } - - 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() - } - } if (localMedia?.uri != null) { LaunchedEffect(localMedia.uri) { val mediaItem = MediaItem.fromUri(localMedia.uri) @@ -263,16 +238,72 @@ private fun ExoPlayerMediaVideoView( ) } - OnLifecycleEvent { _, event -> - when (event) { - Lifecycle.Event.ON_CREATE -> exoPlayer.addListener(playerListener) - Lifecycle.Event.ON_RESUME -> exoPlayer.prepare() - Lifecycle.Event.ON_PAUSE -> exoPlayer.pause() - Lifecycle.Event.ON_DESTROY -> { - exoPlayer.release() - exoPlayer.removeListener(playerListener) + LaunchedEffect(exoPlayer.isPlaying) { + if (exoPlayer.isPlaying) { + while (true) { + mediaPlayerControllerState = mediaPlayerControllerState.copy( + progressInMillis = exoPlayer.currentPosition, + ) + delay(200) } - else -> Unit + } else { + // Ensure we render the final state + mediaPlayerControllerState = mediaPlayerControllerState.copy( + progressInMillis = exoPlayer.currentPosition, + ) + } + } + + ExoPlayerLifecycleHelper( + exoPlayer = exoPlayer, + autoplay = autoplay, + isDisplayed = isDisplayed, + playerListener = playerListener, + mediaPlayerControllerState = mediaPlayerControllerState, + ) +} + +@OptIn(UnstableApi::class) +@Composable +private fun ExoPlayerLifecycleHelper( + exoPlayer: ExoPlayer, + autoplay: Boolean, + isDisplayed: Boolean, + playerListener: Player.Listener, + mediaPlayerControllerState: MediaPlayerControllerState, +) { + // Prepare and release the exoPlayer with the composable lifecycle + DisposableEffect(Unit) { + Timber.d("ExoPlayerMediaVideoView DisposableEffect: initializing exoPlayer") + exoPlayer.addListener(playerListener) + exoPlayer.prepare() + + onDispose { + Timber.d("Disposing exoplayer") + if (!exoPlayer.isReleased) { + exoPlayer.removeListener(playerListener) + exoPlayer.release() + } + } + } + + 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() + } + } + + // Pause playback when lifecycle is paused + OnLifecycleEvent { _, event -> + if (event == Lifecycle.Event.ON_PAUSE && exoPlayer.isPlaying) { + exoPlayer.pause() } } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidation.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidation.kt index 4b54a401fa..009218d755 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidation.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidation.kt @@ -8,13 +8,14 @@ package io.element.android.libraries.mediaviewer.impl.util import android.webkit.MimeTypeMap -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor -import javax.inject.Inject @ContributesBinding(AppScope::class) -class FileExtensionExtractorWithValidation @Inject constructor() : FileExtensionExtractor { +@Inject +class FileExtensionExtractorWithValidation : FileExtensionExtractor { override fun extractFromName(name: String): String { val fileExtension = name.substringAfterLast('.', "") // Makes sure the extension is known by the system, otherwise default to binary extension. 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 c5bca59db7..cb9743bf97 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt @@ -13,9 +13,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ForcedDarkElementTheme import io.element.android.features.viewfolder.api.TextFileViewer import io.element.android.libraries.architecture.inputs @@ -33,7 +33,8 @@ import io.element.android.libraries.mediaviewer.impl.model.hasEvent import io.element.android.services.toolbox.api.systemclock.SystemClock @ContributesNode(RoomScope::class) -class MediaViewerNode @AssistedInject constructor( +@AssistedInject +class MediaViewerNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: MediaViewerPresenter.Factory, 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 f900510321..6f59bc3ff7 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 @@ -21,9 +21,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -49,7 +49,8 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import io.element.android.libraries.androidutils.R as UtilsR -class MediaViewerPresenter @AssistedInject constructor( +@AssistedInject +class MediaViewerPresenter( @Assisted private val inputs: MediaViewerEntryPoint.Params, @Assisted private val navigator: MediaViewerNavigator, @Assisted private val dataSource: MediaViewerDataSource, @@ -57,7 +58,7 @@ class MediaViewerPresenter @AssistedInject constructor( private val localMediaActions: LocalMediaActions, ) : Presenter { @AssistedFactory - interface Factory { + fun interface Factory { fun create( inputs: MediaViewerEntryPoint.Params, navigator: MediaViewerNavigator, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/PagerKeysHandler.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/PagerKeysHandler.kt index 2c73ab2657..03d37690ed 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/PagerKeysHandler.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/PagerKeysHandler.kt @@ -7,9 +7,9 @@ package io.element.android.libraries.mediaviewer.impl.viewer +import dev.zacsweers.metro.Inject import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.impl.model.eventId -import javax.inject.Inject /** * x and y are loading items. @@ -35,7 +35,8 @@ import javax.inject.Inject * -1 0 1 2 3 4 5 6 * (keyOffset = -1) */ -class PagerKeysHandler @Inject constructor() { +@Inject +class PagerKeysHandler { private data class Data( val mediaItems: List, val keyOffset: Long, diff --git a/libraries/mediaviewer/impl/src/main/res/values-bg/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-bg/translations.xml index e6874e6743..8607121680 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-bg/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-bg/translations.xml @@ -1,5 +1,6 @@ + "Зареждане на медийни файлове…" "Файлове" "Медия" "Медия и файлове" 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 e27fd3f1cc..ab3634e777 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-de/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-de/translations.xml @@ -1,15 +1,15 @@ - "Diese Datei wird aus dem Chatroom entfernt und die Mitglieder werden keinen Zugriff mehr darauf haben." + "Diese Datei wird aus dem Chat entfernt und die Mitglieder werden keinen Zugriff mehr darauf haben." "Datei löschen?" - "Überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut." - "Dokumente, Audiodateien und Sprachnachrichten, die in diesen Chatroom hochgeladen wurden, werden hier angezeigt." + "Überprüfe deine Internetverbindung und versuche es erneut." + "Dokumente, Audiodateien und Sprachnachrichten, die in diesen Chat hochgeladen wurden, werden hier angezeigt." "Es wurden noch keine Dateien hochgeladen" "Dateien werden geladen…" "Medien werden geladen…" "Dateien" "Medien" - "In diesen Chatroom hochgeladene Bilder und Videos werden hier angezeigt." + "In diesen Chat hochgeladene Bilder und Videos werden hier angezeigt." "Noch keine Medien hochgeladen" "Medien und Dateien" "Dateiformat" diff --git a/libraries/mediaviewer/impl/src/main/res/values-ko/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..3362171d27 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,21 @@ + + + "이 파일은 방에서 삭제되며, 회원들은 더 이상 액세스할 수 없습니다." + "파일을 삭제하시겠습니까?" + "인터넷 연결을 확인하고 다시 시도해 주세요." + "이 방에 업로드된 문서, 오디오 파일 및 음성 메시지가 여기에 표시됩니다." + "아직 업로드된 파일이 없습니다." + "파일 로딩 중…" + "미디어 로딩 중…" + "파일" + "미디어" + "이 방에 업로드된 이미지와 동영상은 여기에 표시됩니다." + "아직 미디어가 업로드되지 않았습니다." + "미디어 및 파일" + "파일 형식" + "파일 명" + "더 이상 표시할 파일이 없습니다" + "더 이상 보여줄 미디어가 없습니다" + "에 의해 업로드됨" + "에 업로드됨" + 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 index 061b43e5ea..284e7db244 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-pt-rBR/translations.xml @@ -2,17 +2,17 @@ "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" + "Verifique sua conexão à internet e tente novamente." + "Os documentos, arquivos de áudio e mensagens de voz enviados nesta sala serão exibidos aqui." + "Nenhum arquivo enviado ainda" "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" + "As imagens e os vídeos enviados nesta sala serão exibidos aqui." + "Nenhuma mídia enviada ainda" "Mídia e arquivos" - "Formato de arquivo" + "Formato do arquivo" "Nome do arquivo" "Não há mais arquivos para mostrar" "Não há mais mídia para mostrar" diff --git a/libraries/mediaviewer/impl/src/main/res/values-ro/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..bfd1d49c2b --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,21 @@ + + + "Acest fișier va fi șters din cameră și membrii nu vor mai avea acces la el." + "Ştergeţi fişierul?" + "Verificați conexiunea la internet și încercați din nou." + "Documentele, fișierele audio și mesajele vocale încărcate în această cameră vor fi afișate aici." + "Nu există încă fișiere încărcate" + "Se încarcă fișierele…" + "Se încarcă media…" + "Fișiere" + "Media" + "Imaginile și videoclipurile încărcate în această cameră vor fi afișate aici." + "Nu a fost încărcat încă niciun fișier media" + "Media și fișiere" + "Format fişier" + "Nume fișier" + "Nu mai există fișiere de afișat" + "Nu mai există conținut media de afișat" + "Încărcat de" + "Încărcat la" + diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPointTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPointTest.kt new file mode 100644 index 0000000000..0f8b0fedfb --- /dev/null +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPointTest.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.android.libraries.mediaviewer.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint +import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint +import io.element.android.libraries.mediaviewer.impl.gallery.root.MediaGalleryFlowNode +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultMediaGalleryEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultMediaGalleryEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + MediaGalleryFlowNode( + buildContext = buildContext, + plugins = plugins, + mediaViewerEntryPoint = object : MediaViewerEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + } + ) + } + val callback = object : MediaGalleryEntryPoint.Callback { + override fun onBackClick() = lambdaError() + override fun onViewInTimeline(eventId: EventId) = lambdaError() + } + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .callback(callback) + .build() + assertThat(result).isInstanceOf(MediaGalleryFlowNode::class.java) + assertThat(result.plugins).contains(callback) + } +} diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt new file mode 100644 index 0000000000..3af2e8cf69 --- /dev/null +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.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.libraries.mediaviewer.impl + +import android.net.Uri +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader +import io.element.android.libraries.mediaplayer.test.FakeAudioFocus +import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint +import io.element.android.libraries.mediaviewer.impl.datasource.createTimelineMediaGalleryDataSource +import io.element.android.libraries.mediaviewer.impl.viewer.MediaViewerNode +import io.element.android.libraries.mediaviewer.impl.viewer.PagerKeysHandler +import io.element.android.libraries.mediaviewer.impl.viewer.createMediaViewerEntryPointParams +import io.element.android.libraries.mediaviewer.impl.viewer.createMediaViewerPresenter +import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import io.element.android.tests.testutils.testCoroutineDispatchers +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class DefaultMediaViewerEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() = runTest { + val entryPoint = DefaultMediaViewerEntryPoint() + val mockMediaUri: Uri = mockk("localMediaUri") + val localMediaFactory = FakeLocalMediaFactory(mockMediaUri) + val parentNode = TestParentNode.create { buildContext, plugins -> + MediaViewerNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { _, _, _ -> + createMediaViewerPresenter( + localMediaFactory = localMediaFactory, + ) + }, + timelineMediaGalleryDataSource = createTimelineMediaGalleryDataSource(), + focusedTimelineMediaGalleryDataSourceFactory = { _, _, _ -> + lambdaError() + }, + mediaLoader = FakeMatrixMediaLoader(), + localMediaFactory = FakeLocalMediaFactory(mockMediaUri), + coroutineDispatchers = testCoroutineDispatchers(), + systemClock = FakeSystemClock(), + pagerKeysHandler = PagerKeysHandler(), + textFileViewer = { _, _ -> lambdaError() }, + audioFocus = FakeAudioFocus(), + ) + } + val callback = object : MediaViewerEntryPoint.Callback { + override fun onDone() = lambdaError() + override fun onViewInTimeline(eventId: EventId) = lambdaError() + } + val params = createMediaViewerEntryPointParams() + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .callback(callback) + .build() + assertThat(result).isInstanceOf(MediaViewerNode::class.java) + assertThat(result.plugins).contains(params) + assertThat(result.plugins).contains(callback) + } + + @Test + fun `test node builder avatar`() = runTest { + val entryPoint = DefaultMediaViewerEntryPoint() + val mockMediaUri: Uri = mockk("localMediaUri") + val localMediaFactory = FakeLocalMediaFactory(mockMediaUri) + val parentNode = TestParentNode.create { buildContext, plugins -> + MediaViewerNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { _, _, _ -> + createMediaViewerPresenter( + localMediaFactory = localMediaFactory, + ) + }, + timelineMediaGalleryDataSource = createTimelineMediaGalleryDataSource(), + focusedTimelineMediaGalleryDataSourceFactory = { _, _, _ -> + lambdaError() + }, + mediaLoader = FakeMatrixMediaLoader(), + localMediaFactory = FakeLocalMediaFactory(mockMediaUri), + coroutineDispatchers = testCoroutineDispatchers(), + systemClock = FakeSystemClock(), + pagerKeysHandler = PagerKeysHandler(), + textFileViewer = { _, _ -> lambdaError() }, + audioFocus = FakeAudioFocus(), + ) + } + val callback = object : MediaViewerEntryPoint.Callback { + override fun onDone() = lambdaError() + override fun onViewInTimeline(eventId: EventId) = lambdaError() + } + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .avatar( + filename = "fn", + avatarUrl = "avatarUrl", + ) + .callback(callback) + .build() + assertThat(result).isInstanceOf(MediaViewerNode::class.java) + assertThat(result.plugins).contains( + MediaViewerEntryPoint.Params( + mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia, + eventId = null, + mediaInfo = MediaInfo( + filename = "fn", + fileSize = null, + caption = null, + mimeType = MimeTypes.Images, + formattedFileSize = "", + fileExtension = "", + senderId = UserId("@dummy:server.org"), + senderName = null, + senderAvatar = null, + dateSent = null, + dateSentFull = null, + waveform = null, + duration = null, + ), + mediaSource = MediaSource(url = "avatarUrl"), + thumbnailSource = null, + canShowInfo = false, + ) + ) + assertThat(result.plugins).contains(callback) + } +} 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 7e9f1870f5..771c82e4a2 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 @@ -255,19 +255,19 @@ class TimelineMediaGalleryDataSourceTest { ) } } +} - private fun TestScope.createTimelineMediaGalleryDataSource( - room: JoinedRoom = FakeJoinedRoom( - liveTimeline = FakeTimeline(), - ), - ): TimelineMediaGalleryDataSource { - return TimelineMediaGalleryDataSource( - room = room, - mediaTimeline = LiveMediaTimeline(room), - timelineMediaItemsFactory = createTimelineMediaItemsFactory(), - mediaItemsPostProcessor = MediaItemsPostProcessor(), - ) - } +internal fun TestScope.createTimelineMediaGalleryDataSource( + room: JoinedRoom = FakeJoinedRoom( + liveTimeline = FakeTimeline(), + ), +): TimelineMediaGalleryDataSource { + return TimelineMediaGalleryDataSource( + room = room, + mediaTimeline = LiveMediaTimeline(room), + timelineMediaItemsFactory = createTimelineMediaItemsFactory(), + mediaItemsPostProcessor = MediaItemsPostProcessor(), + ) } fun TestScope.createTimelineMediaItemsFactory() = TimelineMediaItemsFactory( 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 80005facd4..f126dab2b5 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 @@ -10,7 +10,9 @@ package io.element.android.libraries.mediaviewer.impl.gallery import android.net.Uri import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncData 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.core.EventId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.Timeline @@ -29,6 +31,7 @@ import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetSta import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory +import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -147,8 +150,8 @@ class MediaGalleryPresenterTest { val presenter = createMediaGalleryPresenter( room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - sessionId = A_USER_ID, - initialRoomInfo = aRoomInfo(name = A_ROOM_NAME), + sessionId = A_USER_ID, + initialRoomInfo = aRoomInfo(name = A_ROOM_NAME), canRedactOtherResult = { Result.success(canDeleteOther) }, ), createTimelineResult = { Result.success(FakeTimeline()) } @@ -223,23 +226,122 @@ class MediaGalleryPresenterTest { } @Test - fun `present - share item`() = runTest { + fun `present - share item - item not found`() = runTest { val presenter = createMediaGalleryPresenter() presenter.test { val initialState = awaitFirstItem() initialState.eventSink(MediaGalleryEvents.Share(AN_EVENT_ID)) } - // TODO Add more test on this part } @Test - fun `present - save on disk`() = runTest { + fun `present - share item - item found`() = runTest { + val mediaGalleryDataSource = FakeMediaGalleryDataSource( + startLambda = { }, + ) + mediaGalleryDataSource.emitGroupedMediaItems( + AsyncData.Success( + aGroupedMediaItems( + imageAndVideoItems = listOf(aMediaItemImage(eventId = AN_EVENT_ID)), + fileItems = emptyList(), + ) + ) + ) + val presenter = createMediaGalleryPresenter( + mediaGalleryDataSource = mediaGalleryDataSource, + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink(MediaGalleryEvents.Share(AN_EVENT_ID)) + val finalState = awaitItem() + assertThat(finalState.snackbarMessage).isNull() + } + } + + @Test + fun `present - share item - item found - download error`() = runTest { + val mediaGalleryDataSource = FakeMediaGalleryDataSource( + startLambda = { }, + ) + mediaGalleryDataSource.emitGroupedMediaItems( + AsyncData.Success( + aGroupedMediaItems( + imageAndVideoItems = listOf(aMediaItemImage(eventId = AN_EVENT_ID)), + fileItems = emptyList(), + ) + ) + ) + val presenter = createMediaGalleryPresenter( + mediaGalleryDataSource = mediaGalleryDataSource, + matrixMediaLoader = FakeMatrixMediaLoader().apply { shouldFail = true }, + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink(MediaGalleryEvents.Share(AN_EVENT_ID)) + skipItems(1) + val finalState = awaitItem() + assertThat(finalState.snackbarMessage).isInstanceOf(SnackbarMessage::class.java) + } + } + + @Test + fun `present - save on disk - item not found`() = runTest { val presenter = createMediaGalleryPresenter() presenter.test { val initialState = awaitFirstItem() initialState.eventSink(MediaGalleryEvents.SaveOnDisk(AN_EVENT_ID)) } - // TODO Add more test on this part + } + + @Test + fun `present - save on disk - item found`() = runTest { + val mediaGalleryDataSource = FakeMediaGalleryDataSource( + startLambda = { }, + ) + mediaGalleryDataSource.emitGroupedMediaItems( + AsyncData.Success( + aGroupedMediaItems( + imageAndVideoItems = listOf(aMediaItemImage(eventId = AN_EVENT_ID)), + fileItems = emptyList(), + ) + ) + ) + val presenter = createMediaGalleryPresenter( + mediaGalleryDataSource = mediaGalleryDataSource, + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink(MediaGalleryEvents.SaveOnDisk(AN_EVENT_ID)) + skipItems(1) + val finalState = awaitItem() + assertThat(finalState.snackbarMessage?.messageResId).isEqualTo(CommonStrings.common_file_saved_on_disk_android) + } + } + + @Test + fun `present - save on disk - item found - download error`() = runTest { + val mediaGalleryDataSource = FakeMediaGalleryDataSource( + startLambda = { }, + ) + mediaGalleryDataSource.emitGroupedMediaItems( + AsyncData.Success( + aGroupedMediaItems( + imageAndVideoItems = listOf(aMediaItemImage(eventId = AN_EVENT_ID)), + fileItems = emptyList(), + ) + ) + ) + val presenter = createMediaGalleryPresenter( + mediaGalleryDataSource = mediaGalleryDataSource, + matrixMediaLoader = FakeMatrixMediaLoader().apply { shouldFail = true }, + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink(MediaGalleryEvents.SaveOnDisk(AN_EVENT_ID)) + skipItems(1) + val finalState = awaitItem() + assertThat(finalState.snackbarMessage).isInstanceOf(SnackbarMessage::class.java) + } } @Test 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 e01eb53efb..b9964d8d3d 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 @@ -29,6 +29,7 @@ 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 +import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.mediaviewer.impl.R import io.element.android.libraries.mediaviewer.impl.datasource.FakeMediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource @@ -78,12 +79,13 @@ class MediaViewerPresenterTest { @Test fun `present - initial state null Event`() = runTest { val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + ) ) ) - ) presenter.test { val initialState = awaitFirstItem() assertThat(initialState.listData).isEmpty() @@ -97,13 +99,14 @@ class MediaViewerPresenterTest { @Test fun `present - initial state cannot show info`() = runTest { val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, canShowInfo = false, room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + ) ) ) - ) presenter.test { val initialState = awaitFirstItem() assertThat(initialState.listData).isEmpty() @@ -117,13 +120,14 @@ class MediaViewerPresenterTest { @Test fun `present - initial state Event`() = runTest { val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, eventId = AN_EVENT_ID, room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + ) ) ) - ) presenter.test { val initialState = awaitFirstItem() assertThat(initialState.listData).isEmpty() @@ -137,14 +141,15 @@ class MediaViewerPresenterTest { @Test fun `present - initial state Event from other`() = runTest { val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, eventId = AN_EVENT_ID, room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - sessionId = A_SESSION_ID_2, - canRedactOtherResult = { Result.success(false) }, + sessionId = A_SESSION_ID_2, + canRedactOtherResult = { Result.success(false) }, + ) ) ) - ) presenter.test { val initialState = awaitFirstItem() assertThat(initialState.listData).isEmpty() @@ -161,6 +166,7 @@ class MediaViewerPresenterTest { startLambda = { }, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, mediaGalleryDataSource = mediaGalleryDataSource, ) val anImage = aMediaItemImage() @@ -192,6 +198,7 @@ class MediaViewerPresenterTest { startLambda = { }, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, mediaGalleryDataSource = mediaGalleryDataSource, ) val anImage = aMediaItemImage( @@ -224,10 +231,13 @@ class MediaViewerPresenterTest { startLambda = { }, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, mediaGalleryDataSource = mediaGalleryDataSource, - room = FakeJoinedRoom(baseRoom = FakeBaseRoom( - canRedactOwnResult = { Result.success(true) }, - )) + room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canRedactOwnResult = { Result.success(true) }, + ) + ) ) val anImage = aMediaItemImage( mediaSourceUrl = aUrl, @@ -266,6 +276,7 @@ class MediaViewerPresenterTest { startLambda = { }, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, mediaGalleryDataSource = mediaGalleryDataSource, ) val anImage = aMediaItemImage( @@ -298,6 +309,7 @@ class MediaViewerPresenterTest { startLambda = { }, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, mediaGalleryDataSource = mediaGalleryDataSource, ) val anImage = aMediaItemImage( @@ -330,6 +342,7 @@ class MediaViewerPresenterTest { startLambda = { }, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, mediaGalleryDataSource = mediaGalleryDataSource, ) val anImage = aMediaItemImage( @@ -362,6 +375,7 @@ class MediaViewerPresenterTest { startLambda = { }, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, mediaGalleryDataSource = mediaGalleryDataSource, ) val anImage = aMediaItemImage( @@ -394,6 +408,7 @@ class MediaViewerPresenterTest { startLambda = { }, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, mediaGalleryDataSource = mediaGalleryDataSource, ) val anImage = aMediaItemImage( @@ -441,6 +456,7 @@ class MediaViewerPresenterTest { startLambda = { }, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, room = FakeJoinedRoom( liveTimeline = timeline, baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }), @@ -498,6 +514,7 @@ class MediaViewerPresenterTest { startLambda = { }, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, mediaGalleryDataSource = mediaGalleryDataSource, ) val anImage = aMediaItemImage( @@ -549,6 +566,7 @@ class MediaViewerPresenterTest { startLambda = { }, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, mode = mode, mediaGalleryDataSource = mediaGalleryDataSource, ) @@ -620,6 +638,7 @@ class MediaViewerPresenterTest { startLambda = { }, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, mode = mode, mediaGalleryDataSource = mediaGalleryDataSource, ) @@ -674,6 +693,7 @@ class MediaViewerPresenterTest { startLambda = { }, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, mediaGalleryDataSource = mediaGalleryDataSource, ) presenter.test { @@ -714,6 +734,7 @@ class MediaViewerPresenterTest { loadMoreLambda = loadMoreLambda, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, mediaGalleryDataSource = mediaGalleryDataSource, ) val anImage = aMediaItemImage( @@ -744,6 +765,7 @@ class MediaViewerPresenterTest { onViewInTimelineClickLambda = onViewInTimelineClickLambda, ) val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, mediaViewerNavigator = navigator, room = FakeJoinedRoom( baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }), @@ -764,42 +786,53 @@ class MediaViewerPresenterTest { private suspend fun ReceiveTurbine.awaitFirstItem(): T { return awaitItem() } - - private fun TestScope.createMediaViewerPresenter( - eventId: EventId? = null, - mode: MediaViewerEntryPoint.MediaViewerMode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia, - matrixMediaLoader: FakeMatrixMediaLoader = FakeMatrixMediaLoader(), - localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(), - mediaGalleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource( - startLambda = { }, - ), - canShowInfo: Boolean = true, - mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(), - room: JoinedRoom = FakeJoinedRoom( - liveTimeline = FakeTimeline(), - ), - ): MediaViewerPresenter { - return MediaViewerPresenter( - inputs = MediaViewerEntryPoint.Params( - mode = mode, - eventId = eventId, - mediaInfo = TESTED_MEDIA_INFO, - mediaSource = aMediaSource(), - thumbnailSource = null, - canShowInfo = canShowInfo, - ), - navigator = mediaViewerNavigator, - dataSource = MediaViewerDataSource( - mode = mode, - dispatcher = testCoroutineDispatchers().computation, - galleryDataSource = mediaGalleryDataSource, - mediaLoader = matrixMediaLoader, - localMediaFactory = localMediaFactory, - systemClock = FakeSystemClock(), - pagerKeysHandler = PagerKeysHandler(), - ), - room = room, - localMediaActions = localMediaActions, - ) - } } + +internal fun TestScope.createMediaViewerPresenter( + localMediaFactory: LocalMediaFactory, + eventId: EventId? = null, + mode: MediaViewerEntryPoint.MediaViewerMode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia, + matrixMediaLoader: FakeMatrixMediaLoader = FakeMatrixMediaLoader(), + localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(), + mediaGalleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource( + startLambda = { }, + ), + canShowInfo: Boolean = true, + mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(), + room: JoinedRoom = FakeJoinedRoom( + liveTimeline = FakeTimeline(), + ), +): MediaViewerPresenter { + return MediaViewerPresenter( + inputs = createMediaViewerEntryPointParams( + eventId = eventId, + mode = mode, + canShowInfo = canShowInfo, + ), + navigator = mediaViewerNavigator, + dataSource = MediaViewerDataSource( + mode = mode, + dispatcher = testCoroutineDispatchers().computation, + galleryDataSource = mediaGalleryDataSource, + mediaLoader = matrixMediaLoader, + localMediaFactory = localMediaFactory, + systemClock = FakeSystemClock(), + pagerKeysHandler = PagerKeysHandler(), + ), + room = room, + localMediaActions = localMediaActions, + ) +} + +internal fun createMediaViewerEntryPointParams( + eventId: EventId? = null, + mode: MediaViewerEntryPoint.MediaViewerMode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia, + canShowInfo: Boolean = true, +) = MediaViewerEntryPoint.Params( + mode = mode, + eventId = eventId, + mediaInfo = TESTED_MEDIA_INFO, + mediaSource = aMediaSource(), + thumbnailSource = null, + canShowInfo = canShowInfo, +) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt index 540f107601..c80aa15428 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt @@ -168,7 +168,7 @@ class SingleMediaGalleryDataSourceTest { assertThat(resultData.fileItems).isEmpty() } - private fun aMediaViewerEntryPointParams( + internal fun aMediaViewerEntryPointParams( mediaInfo: MediaInfo, ) = MediaViewerEntryPoint.Params( mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia, diff --git a/libraries/mediaviewer/test/build.gradle.kts b/libraries/mediaviewer/test/build.gradle.kts index cb7bcf671d..45096be999 100644 --- a/libraries/mediaviewer/test/build.gradle.kts +++ b/libraries/mediaviewer/test/build.gradle.kts @@ -1,3 +1,5 @@ +import extension.testCommonDependencies + /* * Copyright 2023, 2024 New Vector Ltd. * @@ -19,6 +21,5 @@ dependencies { implementation(projects.tests.testutils) implementation(projects.libraries.matrix.api) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) + testCommonDependencies(libs) } diff --git a/libraries/network/build.gradle.kts b/libraries/network/build.gradle.kts index 9b4d905135..b702ff7881 100644 --- a/libraries/network/build.gradle.kts +++ b/libraries/network/build.gradle.kts @@ -1,4 +1,4 @@ -import extension.setupAnvil +import extension.setupDependencyInjection /* * Copyright 2023, 2024 New Vector Ltd. @@ -20,10 +20,9 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { - implementation(libs.dagger) implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt index 17e446bd9f..6ede5a0c48 100644 --- a/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt @@ -7,12 +7,12 @@ package io.element.android.libraries.network -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger import io.element.android.libraries.network.interceptors.UserAgentInterceptor import kotlinx.serialization.json.Json @@ -20,7 +20,7 @@ import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import java.util.concurrent.TimeUnit -@Module +@BindingContainer @ContributesTo(AppScope::class) object NetworkModule { @Provides diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt index 195c25db35..261d7d02ba 100644 --- a/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt @@ -7,22 +7,23 @@ package io.element.android.libraries.network +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.Provider import io.element.android.libraries.core.uri.ensureTrailingSlash import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.kotlinx.serialization.asConverterFactory -import javax.inject.Inject -import javax.inject.Provider -class RetrofitFactory @Inject constructor( +@Inject +class RetrofitFactory( private val okHttpClient: Provider, private val json: Provider, ) { fun create(baseUrl: String): Retrofit = Retrofit.Builder() .baseUrl(baseUrl.ensureTrailingSlash()) - .addConverterFactory(json.get().asConverterFactory("application/json".toMediaType())) - .callFactory { request -> okHttpClient.get().newCall(request) } + .addConverterFactory(json().asConverterFactory("application/json".toMediaType())) + .callFactory { request -> okHttpClient().newCall(request) } .build() } diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/interceptors/UserAgentInterceptor.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/interceptors/UserAgentInterceptor.kt index bddd8151ba..0242774bdd 100644 --- a/libraries/network/src/main/kotlin/io/element/android/libraries/network/interceptors/UserAgentInterceptor.kt +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/interceptors/UserAgentInterceptor.kt @@ -7,13 +7,14 @@ package io.element.android.libraries.network.interceptors +import dev.zacsweers.metro.Inject import io.element.android.libraries.network.headers.HttpHeaders import io.element.android.libraries.network.useragent.UserAgentProvider import okhttp3.Interceptor import okhttp3.Response -import javax.inject.Inject -class UserAgentInterceptor @Inject constructor( +@Inject +class UserAgentInterceptor( private val userAgentProvider: UserAgentProvider, ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/useragent/DefaultUserAgentProvider.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/useragent/DefaultUserAgentProvider.kt index 265a3c6bc7..bf1d2a93c9 100644 --- a/libraries/network/src/main/kotlin/io/element/android/libraries/network/useragent/DefaultUserAgentProvider.kt +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/useragent/DefaultUserAgentProvider.kt @@ -8,16 +8,17 @@ package io.element.android.libraries.network.useragent import android.os.Build -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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.matrix.api.SdkMetadata -import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultUserAgentProvider @Inject constructor( +@Inject +class DefaultUserAgentProvider( private val buildMeta: BuildMeta, private val sdkMeta: SdkMetadata, ) : UserAgentProvider { diff --git a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt index abd83d098f..fc464e9ee2 100644 --- a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt +++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt @@ -8,6 +8,6 @@ package io.element.android.libraries.oidc.api sealed interface OidcAction { - data object GoBack : OidcAction + data class GoBack(val toUnblock: Boolean = false) : OidcAction data class Success(val url: String) : OidcAction } diff --git a/libraries/oidc/impl/build.gradle.kts b/libraries/oidc/impl/build.gradle.kts index c0f8e9d1d6..f3c50ed9ab 100644 --- a/libraries/oidc/impl/build.gradle.kts +++ b/libraries/oidc/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2024 New Vector Ltd. @@ -23,7 +24,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.appconfig) @@ -39,14 +40,7 @@ dependencies { implementation(libs.serialization.json) 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) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.permissions.test) - testImplementation(projects.tests.testutils) } diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt index e975cde261..c49128543a 100644 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt @@ -7,18 +7,19 @@ package io.element.android.libraries.oidc.impl -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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 -import javax.inject.Inject @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) -class DefaultOidcActionFlow @Inject constructor() : OidcActionFlow { +@Inject +class DefaultOidcActionFlow : OidcActionFlow { private val mutableStateFlow = MutableStateFlow(null) override fun post(oidcAction: OidcAction) { diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt index b4dbc0a582..42cdb14851 100644 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt @@ -8,14 +8,15 @@ package io.element.android.libraries.oidc.impl import android.content.Intent -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oidc.api.OidcIntentResolver -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultOidcIntentResolver @Inject constructor( +@Inject +class DefaultOidcIntentResolver( private val oidcUrlParser: OidcUrlParser, ) : OidcIntentResolver { override fun resolve(intent: Intent): OidcAction? { 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 1a41492df6..1e9b6953a8 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,11 +7,11 @@ package io.element.android.libraries.oidc.impl -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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? @@ -22,7 +22,8 @@ fun interface OidcUrlParser { * TODO Find documentation about the format. */ @ContributesBinding(AppScope::class) -class DefaultOidcUrlParser @Inject constructor( +@Inject +class DefaultOidcUrlParser( private val oidcRedirectUrlProvider: OidcRedirectUrlProvider, ) : OidcUrlParser { /** @@ -35,7 +36,7 @@ class DefaultOidcUrlParser @Inject constructor( */ 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("error=access_denied")) return OidcAction.GoBack() if (url.contains("code=")) return OidcAction.Success(url) // Other case not supported, let's crash the app for now 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 index 3b56f28c5a..51017f0af0 100644 --- 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 @@ -24,10 +24,10 @@ class DefaultOidcActionFlowTest { data.add(action) } } - sut.post(OidcAction.GoBack) + sut.post(OidcAction.GoBack()) delay(1) sut.reset() delay(1) - assertThat(data).containsExactly(OidcAction.GoBack, null) + 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 index e48e0c2e1e..48595452d2 100644 --- 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 @@ -29,7 +29,7 @@ class DefaultOidcIntentResolverTest { data = "io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO".toUri() } val result = sut.resolve(intent) - assertThat(result).isEqualTo(OidcAction.GoBack) + assertThat(result).isEqualTo(OidcAction.GoBack()) } @Test 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 index e40424ca0e..7ec03a258e 100644 --- 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 @@ -31,7 +31,7 @@ class DefaultOidcUrlParserTest { fun `test cancel url`() { val sut = createDefaultOidcUrlParser() val aCancelUrl = "$FAKE_REDIRECT_URL?error=access_denied&state=IFF1UETGye2ZA8pO" - assertThat(sut.parse(aCancelUrl)).isEqualTo(OidcAction.GoBack) + assertThat(sut.parse(aCancelUrl)).isEqualTo(OidcAction.GoBack()) } @Test diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionStateProvider.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionStateProvider.kt index 0c811b7265..be9e38c4fb 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionStateProvider.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionStateProvider.kt @@ -16,6 +16,4 @@ interface PermissionStateProvider { suspend fun setPermissionAsked(permission: String, value: Boolean) fun isPermissionAsked(permission: String): Flow - - suspend fun resetPermission(permission: String) } 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 09c149ddb5..1d63c04b2b 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, erteilen Sie bitte die Erlaubnis in den Systemeinstellungen." - "Bitte erteile 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." + "Damit die Anwendung die Kamera verwenden kann, erteile bitte die Berechtigung in den Systemeinstellungen." + "Bitte erteile die Berechtigung in den Systemeinstellungen." + "Damit die App das Mikrofon nutzen kann, gib bitte die Berechtigung in den Systemeinstellungen frei." + "Damit die App Benachrichtigungen anzeigen kann, gib bitte die Berechtigung in den Systemeinstellungen frei." diff --git a/libraries/permissions/api/src/main/res/values-ko/translations.xml b/libraries/permissions/api/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..c1e3b425c0 --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-ko/translations.xml @@ -0,0 +1,7 @@ + + + "애플리케이션이 카메라를 사용할 수 있도록 시스템 설정에서 권한을 허용해주세요." + "시스템 설정에서 권한을 허용해주세요." + "애플리케이션이 마이크를 사용할 수 있도록 시스템 설정에서 권한을 허용해주세요." + "애플리케이션이 알림을 표시할 수 있도록 시스템 설정에서 권한을 허용해주세요." + diff --git a/libraries/permissions/impl/build.gradle.kts b/libraries/permissions/impl/build.gradle.kts index 2b38e54ef8..a4fafda599 100644 --- a/libraries/permissions/impl/build.gradle.kts +++ b/libraries/permissions/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -21,7 +22,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(libs.accompanist.permission) @@ -35,16 +36,13 @@ dependencies { implementation(projects.libraries.troubleshoot.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) + implementation(projects.libraries.preferences.api) implementation(projects.services.toolbox.api) api(projects.libraries.permissions.api) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.permissions.test) + testImplementation(projects.libraries.troubleshoot.test) testImplementation(projects.services.toolbox.test) - testImplementation(projects.tests.testutils) } diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/AccompanistPermissionStateProvider.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/AccompanistPermissionStateProvider.kt index c54d269ece..f2f83e86e5 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/AccompanistPermissionStateProvider.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/AccompanistPermissionStateProvider.kt @@ -13,9 +13,9 @@ import androidx.compose.runtime.Composable import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.rememberPermissionState -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import javax.inject.Inject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject interface ComposablePermissionStateProvider { @Composable @@ -23,7 +23,8 @@ interface ComposablePermissionStateProvider { } @ContributesBinding(AppScope::class) -class AccompanistPermissionStateProvider @Inject constructor() : ComposablePermissionStateProvider { +@Inject +class AccompanistPermissionStateProvider : ComposablePermissionStateProvider { @Composable override fun provide(permission: String, onPermissionResult: (Boolean) -> Unit): PermissionState { return rememberPermissionState( diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionStateProvider.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionStateProvider.kt index 35738e7e61..6902247970 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionStateProvider.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionStateProvider.kt @@ -10,18 +10,19 @@ package io.element.android.libraries.permissions.impl import android.content.Context import android.content.pm.PackageManager import androidx.core.content.ContextCompat -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.SingleIn +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.permissions.api.PermissionStateProvider import io.element.android.libraries.permissions.api.PermissionsStore import kotlinx.coroutines.flow.Flow -import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultPermissionStateProvider @Inject constructor( +@Inject +class DefaultPermissionStateProvider( @ApplicationContext private val context: Context, private val permissionsStore: PermissionsStore, ) : PermissionStateProvider { @@ -39,6 +40,4 @@ class DefaultPermissionStateProvider @Inject constructor( override suspend fun setPermissionAsked(permission: String, value: Boolean) = permissionsStore.setPermissionAsked(permission, value) override fun isPermissionAsked(permission: String): Flow = permissionsStore.isPermissionAsked(permission) - - override suspend fun resetPermission(permission: String) = permissionsStore.resetPermission(permission) } 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 e9072e3666..f12d92a50d 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 @@ -21,12 +21,12 @@ import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.shouldShowRationale -import com.squareup.anvil.annotations.ContributesBinding -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.core.log.logger.LoggerTag -import io.element.android.libraries.di.AppScope import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsState @@ -37,7 +37,8 @@ import timber.log.Timber private val loggerTag = LoggerTag("DefaultPermissionsPresenter") -class DefaultPermissionsPresenter @AssistedInject constructor( +@AssistedInject +class DefaultPermissionsPresenter( @Assisted val permission: String, private val permissionsStore: PermissionsStore, private val composablePermissionStateProvider: ComposablePermissionStateProvider, diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt index ae9eee76e9..bd495b1d4b 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt @@ -7,28 +7,23 @@ package io.element.android.libraries.permissions.impl -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.preferencesDataStore -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.permissions.api.PermissionsStore +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject - -private val Context.dataStore: DataStore by preferencesDataStore(name = "permissions_store") @ContributesBinding(AppScope::class) -class DefaultPermissionsStore @Inject constructor( - @ApplicationContext private val context: Context, +@Inject +class DefaultPermissionsStore( + preferenceDataStoreFactory: PreferenceDataStoreFactory, ) : PermissionsStore { - private val store = context.dataStore + private val store = preferenceDataStoreFactory.create("permissions_store") override suspend fun setPermissionDenied(permission: String, value: Boolean) { store.edit { prefs -> diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/action/AndroidPermissionActions.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/action/AndroidPermissionActions.kt index 1778f0e39a..e2a3701774 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/action/AndroidPermissionActions.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/action/AndroidPermissionActions.kt @@ -8,14 +8,15 @@ package io.element.android.libraries.permissions.impl.action import android.content.Context -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext -import javax.inject.Inject +import io.element.android.libraries.di.annotations.ApplicationContext @ContributesBinding(AppScope::class) -class AndroidPermissionActions @Inject constructor( +@Inject +class AndroidPermissionActions( @ApplicationContext private val context: Context ) : PermissionActions { override fun openSettings() { diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/troubleshoot/NotificationTroubleshootCheckPermissionTest.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/troubleshoot/NotificationTroubleshootCheckPermissionTest.kt index c819c40bb7..eeadb5598c 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/troubleshoot/NotificationTroubleshootCheckPermissionTest.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/troubleshoot/NotificationTroubleshootCheckPermissionTest.kt @@ -9,11 +9,13 @@ package io.element.android.libraries.permissions.impl.troubleshoot import android.Manifest import android.os.Build -import com.squareup.anvil.annotations.ContributesMultibinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.libraries.permissions.api.PermissionStateProvider import io.element.android.libraries.permissions.impl.R import io.element.android.libraries.permissions.impl.action.PermissionActions +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState @@ -21,10 +23,10 @@ import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import io.element.android.services.toolbox.api.strings.StringProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject -@ContributesMultibinding(AppScope::class) -class NotificationTroubleshootCheckPermissionTest @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class NotificationTroubleshootCheckPermissionTest( private val permissionStateProvider: PermissionStateProvider, private val sdkVersionProvider: BuildVersionSdkIntProvider, private val permissionActions: PermissionActions, @@ -53,7 +55,10 @@ class NotificationTroubleshootCheckPermissionTest @Inject constructor( override suspend fun reset() = delegate.reset() - override suspend fun quickFix(coroutineScope: CoroutineScope) { + override suspend fun quickFix( + coroutineScope: CoroutineScope, + navigator: NotificationTroubleshootNavigator, + ) { // Do not bother about asking the permission inline, just lead the user to the settings permissionActions.openSettings() } diff --git a/libraries/permissions/impl/src/main/res/values-bg/translations.xml b/libraries/permissions/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..7c8a783a18 --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,5 @@ + + + "Проверка дали приложението може да показва известия." + "Проверка на разрешенията" + diff --git a/libraries/permissions/impl/src/main/res/values-ko/translations.xml b/libraries/permissions/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..06ed586090 --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,5 @@ + + + "애플리케이션에서 알림을 표시할 수 있는지 확인하세요." + "권한 확인" + diff --git a/libraries/permissions/impl/src/main/res/values-uz/translations.xml b/libraries/permissions/impl/src/main/res/values-uz/translations.xml new file mode 100644 index 0000000000..a7c278258c --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-uz/translations.xml @@ -0,0 +1,5 @@ + + + "Ilova bildirishnomalarni ko‘rsata olishini tekshiring." + "Ruxsatlarni tekshiring" + 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 a530b8dbee..41a7260ca2 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 @@ -8,11 +8,12 @@ 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.action.FakePermissionActions import io.element.android.libraries.permissions.test.FakePermissionStateProvider import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator +import io.element.android.libraries.troubleshoot.test.runAndTestState import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider import kotlinx.coroutines.launch @@ -28,10 +29,7 @@ class NotificationTroubleshootCheckPermissionTestTest { permissionActions = FakePermissionActions(), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() @@ -47,10 +45,7 @@ class NotificationTroubleshootCheckPermissionTestTest { permissionActions = FakePermissionActions(), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() @@ -74,17 +69,14 @@ class NotificationTroubleshootCheckPermissionTestTest { permissionActions = actions, stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true)) // Quick fix - launch { - sut.quickFix(this) + backgroundScope.launch { + sut.quickFix(this, FakeNotificationTroubleshootNavigator()) // Run the test again (IRL it will be done thanks to the resuming of the application) sut.run(this) } @@ -109,13 +101,10 @@ class NotificationTroubleshootCheckPermissionTestTest { permissionActions = actions, stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) - assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true)) sut.reset() assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) } diff --git a/libraries/permissions/noop/build.gradle.kts b/libraries/permissions/noop/build.gradle.kts index 3eebe43b35..343e913b60 100644 --- a/libraries/permissions/noop/build.gradle.kts +++ b/libraries/permissions/noop/build.gradle.kts @@ -1,3 +1,5 @@ +import extension.testCommonDependencies + /* * Copyright 2023, 2024 New Vector Ltd. * @@ -17,10 +19,5 @@ dependencies { implementation(projects.libraries.architecture) api(projects.libraries.permissions.api) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(projects.tests.testutils) + testCommonDependencies(libs) } diff --git a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionStateProvider.kt b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionStateProvider.kt index c97057bc7d..0365ebfbbe 100644 --- a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionStateProvider.kt +++ b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionStateProvider.kt @@ -15,7 +15,6 @@ class FakePermissionStateProvider( private var permissionGranted: Boolean = true, permissionDenied: Boolean = false, permissionAsked: Boolean = false, - private val resetPermissionLambda: (String) -> Unit = {}, ) : PermissionStateProvider { private val permissionDeniedFlow = MutableStateFlow(permissionDenied) private val permissionAskedFlow = MutableStateFlow(permissionAsked) @@ -37,10 +36,4 @@ class FakePermissionStateProvider( } override fun isPermissionAsked(permission: String): Flow = permissionAskedFlow - - override suspend fun resetPermission(permission: String) { - setPermissionAsked(permission, false) - setPermissionDenied(permission, false) - resetPermissionLambda(permission) - } } diff --git a/libraries/preferences/api/build.gradle.kts b/libraries/preferences/api/build.gradle.kts index 49c45b7f90..0e86cfc083 100644 --- a/libraries/preferences/api/build.gradle.kts +++ b/libraries/preferences/api/build.gradle.kts @@ -1,3 +1,5 @@ +import extension.testCommonDependencies + /* * Copyright 2023, 2024 New Vector Ltd. * @@ -18,7 +20,6 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(libs.androidx.datastore.preferences) + testCommonDependencies(libs) testImplementation(projects.libraries.preferences.test) - testImplementation(libs.test.truth) - testImplementation(libs.coroutines.test) } diff --git a/libraries/preferences/impl/build.gradle.kts b/libraries/preferences/impl/build.gradle.kts index 80988f9c07..f63c057183 100644 --- a/libraries/preferences/impl/build.gradle.kts +++ b/libraries/preferences/impl/build.gradle.kts @@ -1,4 +1,4 @@ -import extension.setupAnvil +import extension.setupDependencyInjection /* * Copyright 2023, 2024 New Vector Ltd. @@ -15,11 +15,10 @@ android { namespace = "io.element.android.libraries.preferences.impl" } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.libraries.preferences.api) - implementation(libs.dagger) implementation(libs.androidx.datastore.preferences) implementation(projects.libraries.androidutils) implementation(projects.libraries.di) 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 0dff16ff98..b3890a8a7d 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 @@ -7,27 +7,21 @@ package io.element.android.libraries.preferences.impl.store -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject - -private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_preferences") private val developerModeKey = booleanPreferencesKey("developerMode") private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl") @@ -38,11 +32,12 @@ private val logLevelKey = stringPreferencesKey("logLevel") private val traceLogPacksKey = stringPreferencesKey("traceLogPacks") @ContributesBinding(AppScope::class) -class DefaultAppPreferencesStore @Inject constructor( - @ApplicationContext context: Context, +@Inject +class DefaultAppPreferencesStore( private val buildMeta: BuildMeta, + preferenceDataStoreFactory: PreferenceDataStoreFactory, ) : AppPreferencesStore { - private val store = context.dataStore + private val store = preferenceDataStoreFactory.create("elementx_preferences") override suspend fun setDeveloperModeEnabled(enabled: Boolean) { store.edit { prefs -> diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesDataStoreFactory.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesDataStoreFactory.kt index 648f82b9bd..78fccf0e84 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesDataStoreFactory.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesDataStoreFactory.kt @@ -11,21 +11,35 @@ import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.androidutils.preferences.DefaultPreferencesCorruptionHandlerFactory +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory -import javax.inject.Inject +import java.util.concurrent.ConcurrentHashMap +@SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultPreferencesDataStoreFactory @Inject constructor( +@Inject +class DefaultPreferencesDataStoreFactory( @ApplicationContext private val context: Context, ) : PreferenceDataStoreFactory { + private val dataStoreHolders = ConcurrentHashMap() + private class DataStoreHolder(name: String) { - val Context.dataStore: DataStore by preferencesDataStore(name = name) + val Context.dataStore: DataStore by preferencesDataStore( + name = name, + corruptionHandler = DefaultPreferencesCorruptionHandlerFactory.replaceWithEmpty(), + ) } + override fun create(name: String): DataStore { - return with(DataStoreHolder(name)) { + val holder = dataStoreHolders.getOrPut(name) { + DataStoreHolder(name) + } + return with(holder) { context.dataStore } } diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStoreFactory.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStoreFactory.kt index 3f137feb68..4f24bec89e 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStoreFactory.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStoreFactory.kt @@ -8,10 +8,11 @@ package io.element.android.libraries.preferences.impl.store import android.content.Context -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.SingleIn +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory @@ -19,11 +20,11 @@ import io.element.android.libraries.sessionstorage.api.observer.SessionListener 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 DefaultSessionPreferencesStoreFactory @Inject constructor( +@Inject +class DefaultSessionPreferencesStoreFactory( @ApplicationContext private val context: Context, sessionObserver: SessionObserver, ) : SessionPreferencesStoreFactory { diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/SessionPreferencesModule.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/SessionPreferencesModule.kt index 027c807d45..225ea4e3d2 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/SessionPreferencesModule.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/SessionPreferencesModule.kt @@ -7,25 +7,25 @@ package io.element.android.libraries.preferences.impl.store -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope -import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import kotlinx.coroutines.CoroutineScope -@Module +@BindingContainer @ContributesTo(SessionScope::class) object SessionPreferencesModule { @Provides fun providesSessionPreferencesStore( defaultSessionPreferencesStoreFactory: DefaultSessionPreferencesStoreFactory, - currentSessionIdHolder: CurrentSessionIdHolder, + sessionId: SessionId, @SessionCoroutineScope sessionCoroutineScope: CoroutineScope, ): SessionPreferencesStore { return defaultSessionPreferencesStoreFactory - .get(currentSessionIdHolder.current, sessionCoroutineScope) + .get(sessionId, sessionCoroutineScope) } } diff --git a/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/RoomMemberFixture.kt b/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/RoomMemberFixture.kt index 0c79bb7eeb..e699f3c4b2 100644 --- a/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/RoomMemberFixture.kt +++ b/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/RoomMemberFixture.kt @@ -19,7 +19,6 @@ fun aRoomMember( membership: RoomMembershipState = RoomMembershipState.JOIN, isNameAmbiguous: Boolean = false, powerLevel: Long = 0L, - normalizedPowerLevel: Long = 0L, isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, @@ -30,7 +29,6 @@ fun aRoomMember( membership = membership, isNameAmbiguous = isNameAmbiguous, powerLevel = powerLevel, - normalizedPowerLevel = normalizedPowerLevel, isIgnored = isIgnored, role = role, membershipChangeReason = membershipChangeReason, diff --git a/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/SpaceRoomFixture.kt b/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/SpaceRoomFixture.kt new file mode 100644 index 0000000000..f3b38c241a --- /dev/null +++ b/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/SpaceRoomFixture.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.libraries.previewutils.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.RoomType +import io.element.android.libraries.matrix.api.room.join.JoinRule +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.api.user.MatrixUser + +fun aSpaceRoom( + name: String? = "Space name", + avatarUrl: String? = null, + canonicalAlias: RoomAlias? = null, + childrenCount: Int = 0, + guestCanJoin: Boolean = false, + heroes: List = emptyList(), + joinRule: JoinRule? = null, + numJoinedMembers: Int = 0, + roomId: RoomId = RoomId("!roomId:example.com"), + roomType: RoomType = RoomType.Space, + state: CurrentUserMembership? = null, + topic: String? = null, + worldReadable: Boolean = false, + via: List = emptyList(), +) = SpaceRoom( + name = name, + avatarUrl = avatarUrl, + canonicalAlias = canonicalAlias, + childrenCount = childrenCount, + guestCanJoin = guestCanJoin, + heroes = heroes, + joinRule = joinRule, + numJoinedMembers = numJoinedMembers, + roomId = roomId, + roomType = roomType, + state = state, + topic = topic, + worldReadable = worldReadable, + via = via, +) diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index d256666156..fa0f65cb8f 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -22,10 +23,9 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { - implementation(libs.dagger) implementation(libs.androidx.corektx) implementation(libs.androidx.datastore.preferences) implementation(platform(libs.network.retrofit.bom)) @@ -64,20 +64,15 @@ dependencies { implementation(projects.services.appnavstate.api) implementation(projects.services.toolbox.api) - testImplementation(libs.test.junit) - testImplementation(libs.test.mockk) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs) testImplementation(libs.coil.test) - testImplementation(libs.coroutines.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.pushstore.test) - testImplementation(projects.tests.testutils) + testImplementation(projects.libraries.troubleshoot.test) testImplementation(projects.features.call.test) testImplementation(projects.features.lockscreen.test) testImplementation(projects.services.appnavstate.test) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultGetCurrentPushProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultGetCurrentPushProvider.kt index e0948c7972..9e7ec0d651 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultGetCurrentPushProvider.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultGetCurrentPushProvider.kt @@ -7,16 +7,17 @@ package io.element.android.libraries.push.impl -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.push.api.GetCurrentPushProvider import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.currentSessionId -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultGetCurrentPushProvider @Inject constructor( +@Inject +class DefaultGetCurrentPushProvider( private val pushStoreFactory: UserPushStoreFactory, private val appNavigationStateService: AppNavigationStateService, ) : GetCurrentPushProvider { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt index d628f7153e..45a0e5b4cd 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -7,9 +7,11 @@ package io.element.android.libraries.push.impl -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metro.binding import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.push.api.GetCurrentPushProvider @@ -26,11 +28,11 @@ import io.element.android.libraries.sessionstorage.api.observer.SessionListener import io.element.android.libraries.sessionstorage.api.observer.SessionObserver import kotlinx.coroutines.flow.Flow import timber.log.Timber -import javax.inject.Inject -@ContributesBinding(AppScope::class, boundType = PushService::class) +@ContributesBinding(AppScope::class, binding = binding()) @SingleIn(AppScope::class) -class DefaultPushService @Inject constructor( +@Inject +class DefaultPushService( private val testPush: TestPush, private val userPushStoreFactory: UserPushStoreFactory, private val pushProviders: Set<@JvmSuppressWildcards PushProvider>, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt index 13653db244..7b7a6a33e8 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt @@ -7,12 +7,13 @@ package io.element.android.libraries.push.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.appconfig.PushConfig import io.element.android.libraries.core.extensions.mapFailure 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.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.exception.ClientException @@ -23,14 +24,14 @@ import io.element.android.libraries.pushproviders.api.RegistrationFailure import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import timber.log.Timber -import javax.inject.Inject internal const val DEFAULT_PUSHER_FILE_TAG = "mobile" private val loggerTag = LoggerTag("DefaultPusherSubscriber", LoggerTag.PushLoggerTag) @ContributesBinding(AppScope::class) -class DefaultPusherSubscriber @Inject constructor( +@Inject +class DefaultPusherSubscriber( private val buildMeta: BuildMeta, private val pushClientSecret: PushClientSecret, private val userPushStoreFactory: UserPushStoreFactory, 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 index 4087a2e798..8379289f88 100644 --- 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 @@ -15,12 +15,12 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher import timber.log.Timber -import javax.inject.Inject interface BatteryOptimization { /** @@ -45,7 +45,8 @@ interface BatteryOptimization { } @ContributesBinding(AppScope::class) -class AndroidBatteryOptimization @Inject constructor( +@Inject +class AndroidBatteryOptimization( @ApplicationContext private val context: Context, private val externalIntentLauncher: ExternalIntentLauncher, 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 index 9fa17f0544..3c876ece1d 100644 --- 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 @@ -15,15 +15,16 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.lifecycle.compose.LifecycleResumeEffect +import dev.zacsweers.metro.Inject 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( +@Inject +class BatteryOptimizationPresenter( private val pushDataStore: PushDataStore, private val mutableBatteryOptimizationStore: MutableBatteryOptimizationStore, private val batteryOptimization: BatteryOptimization, 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 bb8b7e1624..bdd1dc6d7c 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 @@ -9,17 +9,17 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.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.di.annotations.ApplicationContext import io.element.android.libraries.push.api.battery.BatteryOptimizationState import io.element.android.libraries.push.impl.battery.BatteryOptimizationPresenter -@Module +@BindingContainer @ContributesTo(AppScope::class) interface PushModule { companion object { 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 index 23ae48ea31..c69a09b31f 100644 --- 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 @@ -11,19 +11,20 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.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( +@Inject +class DefaultPushHistoryService( private val pushDatabase: PushDatabase, private val systemClock: SystemClock, @ApplicationContext context: Context, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryModule.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryModule.kt index 248ba8182d..b1eb45e433 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryModule.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryModule.kt @@ -8,17 +8,17 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.push.impl.PushDatabase import io.element.encrypteddb.SqlCipherDriverFactory import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider -@Module +@BindingContainer @ContributesTo(AppScope::class) object PushHistoryModule { @Provides diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt index 9b8db9d082..11717c081a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt @@ -9,14 +9,14 @@ package io.element.android.libraries.push.impl.notifications import android.service.notification.StatusBarNotification import androidx.core.app.NotificationManagerCompat -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.extensions.runCatchingExceptions -import io.element.android.libraries.di.AppScope 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.notifications.NotificationIdProvider import timber.log.Timber -import javax.inject.Inject interface ActiveNotificationsProvider { fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List @@ -28,7 +28,8 @@ interface ActiveNotificationsProvider { } @ContributesBinding(AppScope::class) -class DefaultActiveNotificationsProvider @Inject constructor( +@Inject +class DefaultActiveNotificationsProvider( private val notificationManager: NotificationManagerCompat, ) : ActiveNotificationsProvider { override fun getNotificationsForSession(sessionId: SessionId): List { 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 4dcb2debc0..689cc8a2ea 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 @@ -7,15 +7,16 @@ package io.element.android.libraries.push.impl.notifications -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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.exception.NotificationResolverException -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.notification.RtcNotificationType 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 @@ -25,7 +26,6 @@ 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 /** @@ -47,7 +47,8 @@ interface CallNotificationEventResolver { } @ContributesBinding(AppScope::class) -class DefaultCallNotificationEventResolver @Inject constructor( +@Inject +class DefaultCallNotificationEventResolver( private val stringProvider: StringProvider, private val appForegroundStateService: AppForegroundStateService, private val clientProvider: MatrixClientProvider, @@ -57,13 +58,13 @@ class DefaultCallNotificationEventResolver @Inject constructor( notificationData: NotificationData, forceNotify: Boolean ): Result = runCatchingExceptions { - val content = notificationData.content as? NotificationContent.MessageLike.CallNotify + val content = notificationData.content as? NotificationContent.MessageLike.RtcNotification ?: throw NotificationResolverException.UnknownError("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) { + if (content.type == RtcNotificationType.RING) { appForegroundStateService.updateHasRingingCall(true) val client = clientProvider.getOrRestore( @@ -89,7 +90,7 @@ class DefaultCallNotificationEventResolver @Inject constructor( }.getOrDefault(false) notificationData.run { - if (content.type == CallNotifyType.RING && isRoomCallActive && !forceNotify) { + if (content.type == RtcNotificationType.RING && isRoomCallActive && !forceNotify) { NotifiableRingingCallEvent( sessionId = sessionId, roomId = roomId, @@ -103,9 +104,10 @@ class DefaultCallNotificationEventResolver @Inject constructor( description = stringProvider.getString(R.string.notification_incoming_call), senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId), roomAvatarUrl = roomAvatarUrl, - callNotifyType = content.type, + rtcNotificationType = content.type, senderId = content.senderId, senderAvatarUrl = senderAvatarUrl, + expirationTimestamp = content.expirationTimestampMillis, ) } else { Timber.d("Event $eventId is call notify but should not ring: $isRoomCallActive, notify: ${content.type}") @@ -123,7 +125,7 @@ class DefaultCallNotificationEventResolver @Inject constructor( roomIsDm = isDm, roomAvatarPath = roomAvatarUrl, senderAvatarPath = senderAvatarUrl, - type = EventType.CALL_NOTIFY, + type = EventType.RTC_NOTIFICATION, ) } } 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 00af4881e9..38a1d4f753 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 @@ -10,13 +10,14 @@ package io.element.android.libraries.push.impl.notifications import android.content.Context import android.net.Uri import androidx.core.content.FileProvider -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.libraries.core.extensions.flatMap 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.di.annotations.ApplicationContext 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 @@ -50,7 +51,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 timber.log.Timber -import javax.inject.Inject private val loggerTag = LoggerTag("DefaultNotifiableEventResolver", LoggerTag.NotificationLoggerTag) @@ -77,7 +77,8 @@ interface NotifiableEventResolver { @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) -class DefaultNotifiableEventResolver @Inject constructor( +@Inject +class DefaultNotifiableEventResolver( private val stringProvider: StringProvider, private val matrixClientProvider: MatrixClientProvider, private val notificationMediaRepoFactory: NotificationMediaRepo.Factory, @@ -198,7 +199,7 @@ class DefaultNotifiableEventResolver @Inject constructor( ) ResolvedPushEvent.Event(notifiableMessageEvent) } - is NotificationContent.MessageLike.CallNotify -> { + is NotificationContent.MessageLike.RtcNotification -> { val notifiableEvent = callNotificationEventResolver.resolveEvent(userId, this).getOrThrow() ResolvedPushEvent.Event(notifiableEvent) } 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 d7d02f765c..545ef59c6d 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 @@ -16,19 +16,20 @@ 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 +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.media.MediaSource 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.api.notifications.NotificationBitmapLoader import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import timber.log.Timber -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultNotificationBitmapLoader @Inject constructor( +@Inject +class DefaultNotificationBitmapLoader( @ApplicationContext private val context: Context, private val sdkIntProvider: BuildVersionSdkIntProvider, ) : NotificationBitmapLoader { 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 e50536b313..41b9f66f38 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 @@ -9,11 +9,12 @@ package io.element.android.libraries.push.impl.notifications import androidx.annotation.VisibleForTesting import androidx.core.app.NotificationManagerCompat -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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 @@ -34,7 +35,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag.NotificationLoggerTag) @@ -45,7 +45,8 @@ private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag. */ @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultNotificationDrawerManager @Inject constructor( +@Inject +class DefaultNotificationDrawerManager( private val notificationManager: NotificationManagerCompat, private val notificationRenderer: NotificationRenderer, private val appNavigationStateService: AppNavigationStateService, 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 d65df65693..dbabb33058 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 @@ -7,17 +7,18 @@ package io.element.android.libraries.push.impl.notifications -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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.push.api.notifications.OnMissedCallNotificationHandler -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultOnMissedCallNotificationHandler @Inject constructor( +@Inject +class DefaultOnMissedCallNotificationHandler( private val matrixClientProvider: MatrixClientProvider, private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager, private val callNotificationEventResolver: CallNotificationEventResolver, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt index 8e206335b9..57bd9bcf49 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.push.impl.notifications +import dev.zacsweers.metro.Inject 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 @@ -14,9 +15,9 @@ import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.systemclock.SystemClock -import javax.inject.Inject -class FallbackNotificationFactory @Inject constructor( +@Inject +class FallbackNotificationFactory( private val clock: SystemClock, private val stringProvider: StringProvider, ) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt index 9f8954cf38..bd85e71b89 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt @@ -7,13 +7,13 @@ package io.element.android.libraries.push.impl.notifications +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.meta.BuildMeta -import javax.inject.Inject /** * Util class for creating notifications action Ids, using the application id. */ -data class NotificationActionIds @Inject constructor( +@Inject data class NotificationActionIds( private val buildMeta: BuildMeta, ) { val join = "${buildMeta.applicationId}.NotificationActions.JOIN_ACTION" diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt index f72b44187b..92029f84e4 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt @@ -10,8 +10,8 @@ package io.element.android.libraries.push.impl.notifications import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.bindings -import javax.inject.Inject /** * Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.). diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverBindings.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverBindings.kt index 594a8b2ce5..0e5588b7b4 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverBindings.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverBindings.kt @@ -7,8 +7,8 @@ package io.element.android.libraries.push.impl.notifications -import com.squareup.anvil.annotations.ContributesTo -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesTo @ContributesTo(AppScope::class) interface NotificationBroadcastReceiverBindings { 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 b664991dfb..c3df89b4a9 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 @@ -8,6 +8,7 @@ package io.element.android.libraries.push.impl.notifications import android.content.Intent +import dev.zacsweers.metro.Inject 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 @@ -31,11 +32,11 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber import java.util.UUID -import javax.inject.Inject private val loggerTag = LoggerTag("NotificationBroadcastReceiverHandler", LoggerTag.NotificationLoggerTag) -class NotificationBroadcastReceiverHandler @Inject constructor( +@Inject +class NotificationBroadcastReceiverHandler( @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val matrixClientProvider: MatrixClientProvider, 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 52c633820b..d5a35b046f 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 @@ -13,8 +13,9 @@ import android.text.style.StyleSpan import androidx.core.text.buildSpannedString import androidx.core.text.inSpans import coil3.ImageLoader -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 @@ -25,7 +26,6 @@ import io.element.android.libraries.push.impl.notifications.model.InviteNotifiab import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import io.element.android.services.toolbox.api.strings.StringProvider -import javax.inject.Inject interface NotificationDataFactory { suspend fun toNotifications( @@ -54,7 +54,8 @@ interface NotificationDataFactory { } @ContributesBinding(AppScope::class) -class DefaultNotificationDataFactory @Inject constructor( +@Inject +class DefaultNotificationDataFactory( private val notificationCreator: NotificationCreator, private val roomGroupMessageCreator: RoomGroupMessageCreator, private val summaryGroupMessageCreator: SummaryGroupMessageCreator, 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 88e6021cc4..34f141c09f 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 @@ -13,11 +13,11 @@ import android.content.Context import android.content.pm.PackageManager import androidx.core.app.ActivityCompat import androidx.core.app.NotificationManagerCompat -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext import timber.log.Timber -import javax.inject.Inject interface NotificationDisplayer { fun showNotificationMessage(tag: String?, id: Int, notification: Notification): Boolean @@ -27,7 +27,8 @@ interface NotificationDisplayer { } @ContributesBinding(AppScope::class) -class DefaultNotificationDisplayer @Inject constructor( +@Inject +class DefaultNotificationDisplayer( @ApplicationContext private val context: Context, private val notificationManager: NotificationManagerCompat ) : NotificationDisplayer { 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 5483c76578..0794efe6fb 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 @@ -7,12 +7,12 @@ package io.element.android.libraries.push.impl.notifications -import com.squareup.anvil.annotations.ContributesBinding -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesBinding 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 import io.element.android.libraries.matrix.api.media.MediaSource @@ -58,7 +58,8 @@ interface NotificationMediaRepo { ): Result } -class DefaultNotificationMediaRepo @AssistedInject constructor( +@AssistedInject +class DefaultNotificationMediaRepo( @CacheDirectory private val cacheDir: File, private val mxcTools: MxcTools, @Assisted private val client: MatrixClient, 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 79d127b009..5d901abbc9 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 @@ -8,6 +8,7 @@ package io.element.android.libraries.push.impl.notifications import coil3.ImageLoader +import dev.zacsweers.metro.Inject 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 @@ -18,11 +19,11 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import timber.log.Timber -import javax.inject.Inject private val loggerTag = LoggerTag("NotificationRenderer", LoggerTag.NotificationLoggerTag) -class NotificationRenderer @Inject constructor( +@Inject +class NotificationRenderer( private val notificationDisplayer: NotificationDisplayer, private val notificationDataFactory: NotificationDataFactory, ) { 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 index a3ba36cfe8..f2031bdda6 100644 --- 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 @@ -7,8 +7,9 @@ package io.element.android.libraries.push.impl.notifications -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.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 @@ -24,7 +25,6 @@ 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 /** @@ -33,7 +33,8 @@ import kotlin.time.Duration.Companion.milliseconds */ @OptIn(ExperimentalCoroutinesApi::class) @SingleIn(AppScope::class) -class NotificationResolverQueue @Inject constructor( +@Inject +class NotificationResolverQueue( private val notifiableEventResolver: NotifiableEventResolver, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ReplyMessageExtractor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ReplyMessageExtractor.kt index 9af2eb9f13..323c32f64a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ReplyMessageExtractor.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ReplyMessageExtractor.kt @@ -9,16 +9,17 @@ package io.element.android.libraries.push.impl.notifications import android.content.Intent import androidx.core.app.RemoteInput -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import javax.inject.Inject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject interface ReplyMessageExtractor { fun getReplyMessage(intent: Intent): String? } @ContributesBinding(AppScope::class) -class AndroidReplyMessageExtractor @Inject constructor() : ReplyMessageExtractor { +@Inject +class AndroidReplyMessageExtractor : ReplyMessageExtractor { override fun getReplyMessage(intent: Intent): String? { return RemoteInput.getResultsFromIntent(intent) ?.getCharSequence(NotificationBroadcastReceiver.KEY_TEXT_REPLY) 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 3e8ddb8798..0ab6ff27b2 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 @@ -10,8 +10,9 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification import android.graphics.Bitmap import coil3.ImageLoader -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader @@ -20,7 +21,6 @@ import io.element.android.libraries.push.impl.notifications.factories.Notificati import io.element.android.libraries.push.impl.notifications.factories.isSmartReplyError import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.services.toolbox.api.strings.StringProvider -import javax.inject.Inject interface RoomGroupMessageCreator { suspend fun createRoomMessage( @@ -33,7 +33,8 @@ interface RoomGroupMessageCreator { } @ContributesBinding(AppScope::class) -class DefaultRoomGroupMessageCreator @Inject constructor( +@Inject +class DefaultRoomGroupMessageCreator( private val bitmapLoader: NotificationBitmapLoader, private val stringProvider: StringProvider, private val notificationCreator: NotificationCreator, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt index cc71d8c122..5c31f03d5d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt @@ -8,13 +8,13 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.services.toolbox.api.strings.StringProvider -import javax.inject.Inject interface SummaryGroupMessageCreator { fun createSummaryNotification( @@ -36,7 +36,8 @@ interface SummaryGroupMessageCreator { * https://developer.android.com/training/notify-user/group */ @ContributesBinding(AppScope::class) -class DefaultSummaryGroupMessageCreator @Inject constructor( +@Inject +class DefaultSummaryGroupMessageCreator( private val stringProvider: StringProvider, private val notificationCreator: NotificationCreator, ) : SummaryGroupMessageCreator { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiver.kt index e78b618b80..442562fe2f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiver.kt @@ -10,9 +10,9 @@ package io.element.android.libraries.push.impl.notifications import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.bindings import io.element.android.libraries.push.impl.troubleshoot.NotificationClickHandler -import javax.inject.Inject class TestNotificationReceiver : BroadcastReceiver() { @Inject lateinit var notificationClickHandler: NotificationClickHandler diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiverBinding.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiverBinding.kt index ddacda354c..c4020c575c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiverBinding.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiverBinding.kt @@ -7,8 +7,8 @@ package io.element.android.libraries.push.impl.notifications -import com.squareup.anvil.annotations.ContributesTo -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesTo @ContributesTo(AppScope::class) interface TestNotificationReceiverBinding { 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 e707f3ad51..da5e0dc785 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 @@ -14,13 +14,13 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.appconfig.NotificationConfig -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn import io.element.android.libraries.push.impl.R import io.element.android.services.toolbox.api.strings.StringProvider -import javax.inject.Inject /* ========================================================================================== * IDs for channels @@ -57,7 +57,8 @@ private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSI @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultNotificationChannels @Inject constructor( +@Inject +class DefaultNotificationChannels( private val notificationManager: NotificationManagerCompat, private val stringProvider: StringProvider, ) : NotificationChannels { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationService.kt index 9fbb67c08c..bda5a593d5 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationService.kt @@ -14,16 +14,17 @@ import android.os.Build import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.libraries.core.coroutine.withPreviousValue import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize -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.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId @@ -32,6 +33,8 @@ import io.element.android.libraries.matrix.ui.media.InitialsAvatarBitmapGenerato import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService import io.element.android.libraries.push.impl.intent.IntentProvider +import io.element.android.libraries.push.impl.notifications.shortcut.createShortcutId +import io.element.android.libraries.push.impl.notifications.shortcut.filterBySession import io.element.android.libraries.sessionstorage.api.observer.SessionListener import io.element.android.libraries.sessionstorage.api.observer.SessionObserver import io.element.android.libraries.ui.strings.CommonStrings @@ -40,11 +43,11 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber -import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultNotificationConversationService @Inject constructor( +@Inject +class DefaultNotificationConversationService( @ApplicationContext private val context: Context, private val intentProvider: IntentProvider, private val bitmapLoader: NotificationBitmapLoader, @@ -102,10 +105,10 @@ class DefaultNotificationConversationService @Inject constructor( targetSize = defaultShortcutIconSize.toLong() )?.let(IconCompat::createWithBitmap) ?: InitialsAvatarBitmapGenerator(useDarkTheme = useDarkTheme) - .generateBitmap(defaultShortcutIconSize, AvatarData(id = roomId.value, name = roomName, size = AvatarSize.RoomHeader)) + .generateBitmap(defaultShortcutIconSize, AvatarData(id = roomId.value, name = roomName, size = AvatarSize.RoomDetailsHeader)) ?.let(IconCompat::createWithAdaptiveBitmap) - val shortcutInfo = ShortcutInfoCompat.Builder(context, "$sessionId-$roomId") + val shortcutInfo = ShortcutInfoCompat.Builder(context, createShortcutId(sessionId, roomId)) .setShortLabel(roomName) .setIcon(icon) .setIntent(intentProvider.getViewRoomIntent(sessionId, roomId, threadId = null)) @@ -126,7 +129,7 @@ class DefaultNotificationConversationService @Inject constructor( } override suspend fun onLeftRoom(sessionId: SessionId, roomId: RoomId) { - val shortcutsToRemove = listOf("$sessionId-$roomId") + val shortcutsToRemove = listOf(createShortcutId(sessionId, roomId)) runCatchingExceptions { ShortcutManagerCompat.removeDynamicShortcuts(context, shortcutsToRemove) if (isRequestPinShortcutSupported) { @@ -180,7 +183,7 @@ class DefaultNotificationConversationService @Inject constructor( private fun onSessionLogOut(sessionId: SessionId) { runCatchingExceptions { val shortcuts = ShortcutManagerCompat.getDynamicShortcuts(context) - val shortcutIdsToRemove = shortcuts.filter { it.id.startsWith(sessionId.value) }.map { it.id } + val shortcutIdsToRemove = shortcuts.filterBySession(sessionId).map { it.id } ShortcutManagerCompat.removeDynamicShortcuts(context, shortcutIdsToRemove) if (isRequestPinShortcutSupported) { 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 d462ebb5f6..3f07e12691 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 @@ -17,12 +17,13 @@ import androidx.core.app.NotificationCompat.MessagingStyle import androidx.core.app.Person import androidx.core.content.res.ResourcesCompat import coil3.ImageLoader -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.appconfig.NotificationConfig import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.ApplicationContext 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.timeline.item.event.EventType @@ -40,8 +41,8 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent +import io.element.android.libraries.push.impl.notifications.shortcut.createShortcutId import io.element.android.services.toolbox.api.strings.StringProvider -import javax.inject.Inject interface NotificationCreator { /** @@ -85,7 +86,8 @@ interface NotificationCreator { } @ContributesBinding(AppScope::class) -class DefaultNotificationCreator @Inject constructor( +@Inject +class DefaultNotificationCreator( @ApplicationContext private val context: Context, private val notificationChannels: NotificationChannels, private val stringProvider: StringProvider, @@ -121,7 +123,7 @@ class DefaultNotificationCreator @Inject constructor( val smallIcon = CommonDrawables.ic_notification - val containsMissedCall = events.any { it.type == EventType.CALL_NOTIFY } + val containsMissedCall = events.any { it.type == EventType.RTC_NOTIFICATION } val channelId = if (containsMissedCall) { notificationChannels.getChannelForIncomingCall(false) } else { @@ -136,7 +138,10 @@ class DefaultNotificationCreator @Inject constructor( // that can be displayed in not disturb mode if white listed (the later will need compat28.x) .setCategory(NotificationCompat.CATEGORY_MESSAGE) // ID of the corresponding shortcut, for conversation features under API 30+ - .setShortcutId(roomInfo.roomId.value) + // Must match those created in the ShortcutInfoCompat.Builder() + // for the notification to appear as a "Conversation": + // https://developer.android.com/develop/ui/views/notifications/conversations + .setShortcutId(createShortcutId(roomInfo.sessionId, roomInfo.roomId)) // Auto-bundling is enabled for 4 or more notifications on API 24+ (N+) // devices and all Wear devices. But we want a custom grouping, so we specify the groupID .setGroup(roomInfo.sessionId.value) @@ -208,8 +213,8 @@ class DefaultNotificationCreator @Inject constructor( } setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId)) - // If any of the events are of call notify type it means a missed call, set the category to the right value - if (events.any { it.type == EventType.CALL_NOTIFY }) { + // If any of the events are of rtc notification type it means a missed call, set the category to the right value + if (events.any { it.type == EventType.RTC_NOTIFICATION }) { setCategory(NotificationCompat.CATEGORY_MISSED_CALL) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt index 108d671fd3..9b36b0d370 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt @@ -10,8 +10,9 @@ package io.element.android.libraries.push.impl.notifications.factories import android.app.PendingIntent import android.content.Context import android.content.Intent +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.uri.createIgnoredUri -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.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 @@ -22,9 +23,9 @@ import io.element.android.libraries.push.impl.notifications.NotificationBroadcas import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo import io.element.android.libraries.push.impl.notifications.TestNotificationReceiver import io.element.android.services.toolbox.api.systemclock.SystemClock -import javax.inject.Inject -class PendingIntentFactory @Inject constructor( +@Inject +class PendingIntentFactory( @ApplicationContext private val context: Context, private val intentProvider: IntentProvider, private val clock: SystemClock, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt index 2d20a766bb..6e3fe95742 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt @@ -11,9 +11,10 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import androidx.core.app.NotificationCompat +import dev.zacsweers.metro.Inject 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.di.annotations.ApplicationContext import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.NotificationActionIds import io.element.android.libraries.push.impl.notifications.NotificationBroadcastReceiver @@ -21,9 +22,9 @@ import io.element.android.libraries.push.impl.notifications.model.InviteNotifiab 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 javax.inject.Inject -class AcceptInvitationActionFactory @Inject constructor( +@Inject +class AcceptInvitationActionFactory( @ApplicationContext private val context: Context, private val actionIds: NotificationActionIds, private val stringProvider: StringProvider, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt index 68dbb96078..1feaee7954 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt @@ -11,18 +11,19 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import androidx.core.app.NotificationCompat +import dev.zacsweers.metro.Inject 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.di.annotations.ApplicationContext import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.NotificationActionIds import io.element.android.libraries.push.impl.notifications.NotificationBroadcastReceiver import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.systemclock.SystemClock -import javax.inject.Inject -class MarkAsReadActionFactory @Inject constructor( +@Inject +class MarkAsReadActionFactory( @ApplicationContext private val context: Context, private val actionIds: NotificationActionIds, private val stringProvider: StringProvider, 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 6a590aeda8..eb54bf9b36 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 @@ -13,9 +13,10 @@ import android.content.Intent import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.RemoteInput +import dev.zacsweers.metro.Inject 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.di.annotations.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 @@ -26,9 +27,9 @@ import io.element.android.libraries.push.impl.notifications.NotificationBroadcas import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.systemclock.SystemClock -import javax.inject.Inject -class QuickReplyActionFactory @Inject constructor( +@Inject +class QuickReplyActionFactory( @ApplicationContext private val context: Context, private val actionIds: NotificationActionIds, private val stringProvider: StringProvider, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt index 259e88b2b8..73b3fc2fd9 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt @@ -11,9 +11,10 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import androidx.core.app.NotificationCompat +import dev.zacsweers.metro.Inject 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.di.annotations.ApplicationContext import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.NotificationActionIds import io.element.android.libraries.push.impl.notifications.NotificationBroadcastReceiver @@ -21,9 +22,9 @@ import io.element.android.libraries.push.impl.notifications.model.InviteNotifiab 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 javax.inject.Inject -class RejectInvitationActionFactory @Inject constructor( +@Inject +class RejectInvitationActionFactory( @ApplicationContext private val context: Context, private val actionIds: NotificationActionIds, private val stringProvider: StringProvider, 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 07e85ab2bf..7d92ad3a8b 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 @@ -11,7 +11,7 @@ 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.CallNotifyType +import io.element.android.libraries.matrix.api.notification.RtcNotificationType data class NotifiableRingingCallEvent( override val sessionId: SessionId, @@ -27,6 +27,7 @@ data class NotifiableRingingCallEvent( val senderDisambiguatedDisplayName: String?, val senderAvatarUrl: String?, val roomAvatarUrl: String? = null, - val callNotifyType: CallNotifyType, + val rtcNotificationType: RtcNotificationType, val timestamp: Long, + val expirationTimestamp: Long, ) : NotifiableEvent diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/shortcut/Utils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/shortcut/Utils.kt new file mode 100644 index 0000000000..6176886dbc --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/shortcut/Utils.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.push.impl.notifications.shortcut + +import androidx.core.content.pm.ShortcutInfoCompat +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId + +internal fun createShortcutId(sessionId: SessionId, roomId: RoomId) = "$sessionId-$roomId" + +internal fun Iterable.filterBySession(sessionId: SessionId): Iterable { + val prefix = "$sessionId-" + return filter { it.id.startsWith(prefix) } +} 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 6967692a46..3a4cf4d6aa 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 @@ -7,15 +7,15 @@ package io.element.android.libraries.push.impl.push -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.features.call.api.CallType 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.matrix.api.exception.NotificationResolverException import io.element.android.libraries.push.impl.history.PushHistoryService import io.element.android.libraries.push.impl.history.onDiagnosticPush @@ -43,13 +43,13 @@ 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( +@Inject +class DefaultPushHandler( private val onNotifiableEventReceived: OnNotifiableEventReceived, private val onRedactedEventReceived: OnRedactedEventReceived, private val incrementPushDataStore: IncrementPushDataStore, @@ -57,7 +57,6 @@ class DefaultPushHandler @Inject constructor( private val userPushStoreFactory: UserPushStoreFactory, private val pushClientSecret: PushClientSecret, private val buildMeta: BuildMeta, - private val matrixAuthenticationService: MatrixAuthenticationService, private val diagnosticPushHandler: DiagnosticPushHandler, private val elementCallEntryPoint: ElementCallEntryPoint, private val notificationChannels: NotificationChannels, @@ -240,32 +239,15 @@ class DefaultPushHandler @Inject constructor( } else { Timber.tag(loggerTag.value).d("## handleInternal()") } - val clientSecret = pushData.clientSecret - // clientSecret should not be null. If this happens, restore default session - 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().also { - if (it == null) { - if (reason.isNotEmpty()) reason += " - " - reason += "Unable to get latest sessionId" - } - } - } + // Get userId from client secret + val userId = pushClientSecret.getUserIdFromSecret(pushData.clientSecret) if (userId == null) { - Timber.w("Unable to get a session") + Timber.w("Unable to get userId from client secret") pushHistoryService.onUnableToRetrieveSession( providerInfo = providerInfo, eventId = pushData.eventId, roomId = pushData.roomId, - reason = reason, + reason = "Unable to get userId from client secret", ) return } @@ -295,6 +277,7 @@ class DefaultPushHandler @Inject constructor( senderName = notifiableEvent.senderDisambiguatedDisplayName, avatarUrl = notifiableEvent.roomAvatarUrl, timestamp = notifiableEvent.timestamp, + expirationTimestamp = notifiableEvent.expirationTimestamp, notificationChannelId = notificationChannels.getChannelForIncomingCall(ring = true), textContent = notifiableEvent.description, ) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/IncrementPushDataStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/IncrementPushDataStore.kt index bc4cdd8831..cf7145e3ef 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/IncrementPushDataStore.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/IncrementPushDataStore.kt @@ -7,17 +7,18 @@ package io.element.android.libraries.push.impl.push -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.push.impl.store.DefaultPushDataStore -import javax.inject.Inject interface IncrementPushDataStore { suspend fun incrementPushCounter() } @ContributesBinding(AppScope::class) -class DefaultIncrementPushDataStore @Inject constructor( +@Inject +class DefaultIncrementPushDataStore( private val defaultPushDataStore: DefaultPushDataStore ) : IncrementPushDataStore { override suspend fun incrementPushCounter() { 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 index 14e2cfd97b..3c5efe70ba 100644 --- 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 @@ -7,10 +7,10 @@ package io.element.android.libraries.push.impl.push -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.push.impl.store.DefaultPushDataStore -import javax.inject.Inject interface MutableBatteryOptimizationStore { suspend fun showBatteryOptimizationBanner() @@ -19,7 +19,8 @@ interface MutableBatteryOptimizationStore { } @ContributesBinding(AppScope::class) -class DefaultMutableBatteryOptimizationStore @Inject constructor( +@Inject +class DefaultMutableBatteryOptimizationStore( private val defaultPushDataStore: DefaultPushDataStore, ) : MutableBatteryOptimizationStore { override suspend fun showBatteryOptimizationBanner() { 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 b311aab4e2..6daee8f137 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 @@ -7,22 +7,23 @@ package io.element.android.libraries.push.impl.push -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import javax.inject.Inject interface OnNotifiableEventReceived { fun onNotifiableEventsReceived(notifiableEvents: List) } @ContributesBinding(AppScope::class) -class DefaultOnNotifiableEventReceived @Inject constructor( +@Inject +class DefaultOnNotifiableEventReceived( private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager, @AppCoroutineScope private val coroutineScope: CoroutineScope, 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 991baba404..dda61f1eb0 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 @@ -14,10 +14,11 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.MessagingStyle import androidx.core.text.buildSpannedString 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.di.annotations.AppCoroutineScope +import io.element.android.libraries.di.annotations.ApplicationContext 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 @@ -27,14 +28,14 @@ import io.element.android.services.toolbox.api.strings.StringProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject interface OnRedactedEventReceived { fun onRedactedEventsReceived(redactions: List) } @ContributesBinding(AppScope::class) -class DefaultOnRedactedEventReceived @Inject constructor( +@Inject +class DefaultOnRedactedEventReceived( private val activeNotificationsProvider: ActiveNotificationsProvider, private val notificationDisplayer: NotificationDisplayer, @AppCoroutineScope 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 57c004ce23..e3384a9b9b 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 @@ -7,6 +7,7 @@ package io.element.android.libraries.push.impl.push +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags @@ -16,10 +17,10 @@ import io.element.android.services.appnavstate.api.AppForegroundStateService import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import timber.log.Timber -import javax.inject.Inject import kotlin.time.Duration.Companion.seconds -class SyncOnNotifiableEvent @Inject constructor( +@Inject +class SyncOnNotifiableEvent( private val matrixClientProvider: MatrixClientProvider, private val featureFlagService: FeatureFlagService, private val appForegroundStateService: AppForegroundStateService, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayApiFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayApiFactory.kt index 7d92ea2b49..e57d98d796 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayApiFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayApiFactory.kt @@ -7,17 +7,18 @@ package io.element.android.libraries.push.impl.pushgateway -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.network.RetrofitFactory -import javax.inject.Inject interface PushGatewayApiFactory { fun create(baseUrl: String): PushGatewayAPI } @ContributesBinding(AppScope::class) -class DefaultPushGatewayApiFactory @Inject constructor( +@Inject +class DefaultPushGatewayApiFactory( private val retrofitFactory: RetrofitFactory, ) : PushGatewayApiFactory { override fun create(baseUrl: String): PushGatewayAPI { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt index 5e64ae17fc..65f5f8a139 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt @@ -6,12 +6,12 @@ */ package io.element.android.libraries.push.impl.pushgateway -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.push.api.gateway.PushGatewayFailure -import javax.inject.Inject interface PushGatewayNotifyRequest { data class Params( @@ -26,7 +26,8 @@ interface PushGatewayNotifyRequest { } @ContributesBinding(AppScope::class) -class DefaultPushGatewayNotifyRequest @Inject constructor( +@Inject +class DefaultPushGatewayNotifyRequest( private val pushGatewayApiFactory: PushGatewayApiFactory, ) : PushGatewayNotifyRequest { override suspend fun execute(params: PushGatewayNotifyRequest.Params) { 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 a4d9413cb1..e84ffaf7f0 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 @@ -7,42 +7,40 @@ package io.element.android.libraries.push.impl.store -import android.content.Context -import androidx.datastore.core.DataStore -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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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.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.preferences.api.store.PreferenceDataStoreFactory import io.element.android.libraries.push.api.history.PushHistoryItem import io.element.android.libraries.push.impl.PushDatabase +import io.element.android.libraries.push.impl.store.DefaultPushDataStore.Companion.BATTERY_OPTIMIZATION_BANNER_STATE_DISMISSED +import io.element.android.libraries.push.impl.store.DefaultPushDataStore.Companion.BATTERY_OPTIMIZATION_BANNER_STATE_INIT +import io.element.android.libraries.push.impl.store.DefaultPushDataStore.Companion.BATTERY_OPTIMIZATION_BANNER_STATE_SHOW import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject -private val Context.dataStore: DataStore by preferencesDataStore(name = "push_store") - -@SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultPushDataStore @Inject constructor( - @ApplicationContext private val context: Context, +@Inject +class DefaultPushDataStore( private val pushDatabase: PushDatabase, private val dateFormatter: DateFormatter, private val dispatchers: CoroutineDispatchers, + preferencesFactory: PreferenceDataStoreFactory, ) : PushDataStore { private val pushCounter = intPreferencesKey("push_counter") + private val dataStore = preferencesFactory.create("push_store") + /** * Integer preference to track the state of the battery optimization banner. * Possible values: @@ -52,24 +50,24 @@ class DefaultPushDataStore @Inject constructor( */ private val batteryOptimizationBannerState = intPreferencesKey("battery_optimization_banner_state") - override val pushCounterFlow: Flow = context.dataStore.data.map { preferences -> + override val pushCounterFlow: Flow = dataStore.data.map { preferences -> preferences[pushCounter] ?: 0 } @Suppress("UnnecessaryParentheses") - override val shouldDisplayBatteryOptimizationBannerFlow: Flow = context.dataStore.data.map { preferences -> + override val shouldDisplayBatteryOptimizationBannerFlow: Flow = dataStore.data.map { preferences -> (preferences[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT) == BATTERY_OPTIMIZATION_BANNER_STATE_SHOW } suspend fun incrementPushCounter() { - context.dataStore.edit { settings -> + dataStore.edit { settings -> val currentCounterValue = settings[pushCounter] ?: 0 settings[pushCounter] = currentCounterValue + 1 } } suspend fun setBatteryOptimizationBannerState(newState: Int) { - context.dataStore.edit { settings -> + dataStore.edit { settings -> val currentValue = settings[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT settings[batteryOptimizationBannerState] = when (currentValue) { BATTERY_OPTIMIZATION_BANNER_STATE_INIT, @@ -106,7 +104,7 @@ class DefaultPushDataStore @Inject constructor( override suspend fun reset() { pushDatabase.pushHistoryQueries.removeAll() - context.dataStore.edit { + dataStore.edit { it.clear() } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/test/TestPush.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/test/TestPush.kt index 08e725d712..c49bc48b6f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/test/TestPush.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/test/TestPush.kt @@ -7,21 +7,22 @@ package io.element.android.libraries.push.impl.test -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.appconfig.PushConfig -import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig -import javax.inject.Inject interface TestPush { suspend fun execute(config: CurrentUserPushConfig) } @ContributesBinding(AppScope::class) -class DefaultTestPush @Inject constructor( +@Inject +class DefaultTestPush( private val pushGatewayNotifyRequest: PushGatewayNotifyRequest, ) : TestPush { override suspend fun execute(config: CurrentUserPushConfig) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/CurrentPushProviderTest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/CurrentPushProviderTest.kt index 159ffa16f7..5033274998 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/CurrentPushProviderTest.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/CurrentPushProviderTest.kt @@ -7,8 +7,9 @@ package io.element.android.libraries.push.impl.troubleshoot -import com.squareup.anvil.annotations.ContributesMultibinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.libraries.push.api.GetCurrentPushProvider import io.element.android.libraries.push.impl.R import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest @@ -17,10 +18,10 @@ import io.element.android.libraries.troubleshoot.api.test.NotificationTroublesho import io.element.android.services.toolbox.api.strings.StringProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject -@ContributesMultibinding(AppScope::class) -class CurrentPushProviderTest @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class CurrentPushProviderTest( private val getCurrentPushProvider: GetCurrentPushProvider, private val stringProvider: StringProvider, ) : NotificationTroubleshootTest { @@ -43,7 +44,7 @@ class CurrentPushProviderTest @Inject constructor( } else { delegate.updateState( description = stringProvider.getString(R.string.troubleshoot_notifications_test_current_push_provider_failure), - status = NotificationTroubleshootTestState.Status.Failure(false) + status = NotificationTroubleshootTestState.Status.Failure() ) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/DiagnosticPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/DiagnosticPushHandler.kt index 51c1ee525e..86cd2f630c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/DiagnosticPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/DiagnosticPushHandler.kt @@ -7,14 +7,15 @@ package io.element.android.libraries.push.impl.troubleshoot -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow -import javax.inject.Inject @SingleIn(AppScope::class) -class DiagnosticPushHandler @Inject constructor() { +@Inject +class DiagnosticPushHandler { private val _state = MutableSharedFlow() val state: SharedFlow = _state diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/IgnoredUsersTest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/IgnoredUsersTest.kt new file mode 100644 index 0000000000..93e855c94d --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/IgnoredUsersTest.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.push.impl.troubleshoot + +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.impl.R +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.toolbox.api.strings.StringProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow + +@ContributesIntoSet(SessionScope::class) +@Inject +class IgnoredUsersTest( + private val matrixClient: MatrixClient, + private val stringProvider: StringProvider, +) : NotificationTroubleshootTest { + override val order = 80 + private val delegate = NotificationTroubleshootTestDelegate( + defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_blocked_users_title), + defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_blocked_users_description), + fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY, + ) + override val state: StateFlow = delegate.state + + override suspend fun run(coroutineScope: CoroutineScope) { + delegate.start() + val ignorerUsers = matrixClient.ignoredUsersFlow.value + if (ignorerUsers.isEmpty()) { + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_blocked_users_result_none), + status = NotificationTroubleshootTestState.Status.Success, + ) + } else { + delegate.updateState( + description = stringProvider.getQuantityString( + R.plurals.troubleshoot_notifications_test_blocked_users_result_some, + ignorerUsers.size, + ignorerUsers.size + ), + status = NotificationTroubleshootTestState.Status.Failure( + hasQuickFix = true, + isCritical = false, + quickFixButtonString = stringProvider.getString(R.string.troubleshoot_notifications_test_blocked_users_quick_fix), + ), + ) + } + } + + override suspend fun quickFix( + coroutineScope: CoroutineScope, + navigator: NotificationTroubleshootNavigator, + ) { + navigator.openIgnoredUsers() + } + + override suspend fun reset() = delegate.reset() +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationClickHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationClickHandler.kt index aa796d10bf..e20876198e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationClickHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationClickHandler.kt @@ -7,14 +7,15 @@ package io.element.android.libraries.push.impl.troubleshoot -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow -import javax.inject.Inject @SingleIn(AppScope::class) -class NotificationClickHandler @Inject constructor() { +@Inject +class NotificationClickHandler { private val _state = MutableSharedFlow(extraBufferCapacity = 1) val state: SharedFlow = _state 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 ee140e7f6c..da390f85f2 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 @@ -7,8 +7,9 @@ package io.element.android.libraries.push.impl.troubleshoot -import com.squareup.anvil.annotations.ContributesMultibinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.NotificationDisplayer import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator @@ -22,11 +23,11 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import timber.log.Timber -import javax.inject.Inject import kotlin.time.Duration.Companion.seconds -@ContributesMultibinding(AppScope::class) -class NotificationTest @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class NotificationTest( private val notificationCreator: NotificationCreator, private val notificationDisplayer: NotificationDisplayer, private val notificationClickHandler: NotificationClickHandler, @@ -53,7 +54,7 @@ class NotificationTest @Inject constructor( } else { delegate.updateState( description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_permission_failure), - status = NotificationTroubleshootTestState.Status.Failure(false) + status = NotificationTroubleshootTestState.Status.Failure() ) } } @@ -80,7 +81,7 @@ class NotificationTest @Inject constructor( notificationDisplayer.dismissDiagnosticNotification() delegate.updateState( description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_failure), - status = NotificationTroubleshootTestState.Status.Failure(false) + status = NotificationTroubleshootTestState.Status.Failure() ) } ) 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 3f392a3a17..5b9a9e2fd1 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 @@ -7,11 +7,13 @@ package io.element.android.libraries.push.impl.troubleshoot -import com.squareup.anvil.annotations.ContributesMultibinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.gateway.PushGatewayFailure import io.element.android.libraries.push.impl.R +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState @@ -24,11 +26,11 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import timber.log.Timber -import javax.inject.Inject import kotlin.time.Duration.Companion.seconds -@ContributesMultibinding(AppScope::class) -class PushLoopbackTest @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class PushLoopbackTest( private val pushService: PushService, private val diagnosticPushHandler: DiagnosticPushHandler, private val clock: SystemClock, @@ -55,7 +57,7 @@ class PushLoopbackTest @Inject constructor( val hasQuickFix = pushService.getCurrentPushProvider()?.canRotateToken() == true delegate.updateState( description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_1), - status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix) + status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = hasQuickFix) ) job.cancel() return @@ -63,7 +65,7 @@ class PushLoopbackTest @Inject constructor( Timber.e(e, "Failed to test push") delegate.updateState( description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_2, e.message), - status = NotificationTroubleshootTestState.Status.Failure(false) + status = NotificationTroubleshootTestState.Status.Failure() ) job.cancel() return @@ -71,7 +73,7 @@ class PushLoopbackTest @Inject constructor( if (!testPushResult) { delegate.updateState( description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_3), - status = NotificationTroubleshootTestState.Status.Failure(false) + status = NotificationTroubleshootTestState.Status.Failure() ) job.cancel() return @@ -92,13 +94,16 @@ class PushLoopbackTest @Inject constructor( job.cancel() delegate.updateState( description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_4), - status = NotificationTroubleshootTestState.Status.Failure(false) + status = NotificationTroubleshootTestState.Status.Failure() ) } ) } - override suspend fun quickFix(coroutineScope: CoroutineScope) { + override suspend fun quickFix( + coroutineScope: CoroutineScope, + navigator: NotificationTroubleshootNavigator, + ) { delegate.start() pushService.getCurrentPushProvider()?.rotateToken() run(coroutineScope) 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 5a30db6f59..8e4d1db639 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 @@ -7,8 +7,9 @@ package io.element.android.libraries.push.impl.troubleshoot -import com.squareup.anvil.annotations.ContributesMultibinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.libraries.push.impl.R import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest @@ -17,10 +18,10 @@ import io.element.android.libraries.troubleshoot.api.test.NotificationTroublesho import io.element.android.services.toolbox.api.strings.StringProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject -@ContributesMultibinding(AppScope::class) -class PushProvidersTest @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class PushProvidersTest( pushProviders: Set<@JvmSuppressWildcards PushProvider>, private val stringProvider: StringProvider, ) : NotificationTroubleshootTest { @@ -47,7 +48,7 @@ class PushProvidersTest @Inject constructor( } else { delegate.updateState( description = stringProvider.getString(R.string.troubleshoot_notifications_test_detect_push_provider_failure), - status = NotificationTroubleshootTestState.Status.Failure(false) + status = NotificationTroubleshootTestState.Status.Failure() ) } } 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 ca1d5c08c8..3de0fd470b 100644 --- a/libraries/push/impl/src/main/res/values-bg/translations.xml +++ b/libraries/push/impl/src/main/res/values-bg/translations.xml @@ -1,5 +1,7 @@ + "Обаждане" + "Слушане за събития" "Шумни известия" "Безшумни известия" @@ -10,12 +12,14 @@ "%d известие" "%d известия" + "Имате нови съобщения." "** Неуспешно изпращане - моля, отворете стаята" "Присъединяване" "%d покана" "%d покани" + "Поканиха ви за чат" "Ви спомена: %1$s" "Нови съобщения" @@ -26,8 +30,14 @@ "Отбелязване като прочетено" "Бърз отговор" "Ви покани да се присъедините към стаята" + "Аз" + "Преглеждате известието! Кликнете върху мен!" "%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" @@ -35,5 +45,20 @@ "%d стая" "%d стаи" + "Синхронизация на заден план" + "Услуги на Google" + "Не са намерени валидни услуги на Google Play. Известията може да не работят правилно." + "Проверка на блокирани потребители" + "Преглед на блокираните потребители" + "Няма блокирани потребители." + "Блокирани потребители" + "Получаване на името на текущия доставчик." + "Приложението е изградено с поддръжка за: %1$s" + "Проверка дали приложението може да показва известия." + "Известието не е било кликнато." + "Не може да се покаже известието." + "Известието беше натиснато!" + "Показване на известие" + "Моля, натиснете известието, за да продължите теста." "Грешка: %1$s" 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 d072a1c3b1..002c6d18ac 100644 --- a/libraries/push/impl/src/main/res/values-cs/translations.xml +++ b/libraries/push/impl/src/main/res/values-cs/translations.xml @@ -60,6 +60,15 @@ "Synchronizace na pozadí" "Služby Google" "Nebyly nalezeny žádné funkční služby Google Play. Oznámení nemusí fungovat správně." + "Kontrola blokovaných uživatelů" + "Zobrazit blokované uživatele" + "Žádní uživatelé nejsou blokováni." + + "Zablokovali jste %1$d uživatele. Nebudete dostávat oznámení od tohoto uživatele." + "Zablokovali jste %1$d uživatele. Nebudete dostávat oznámení od těchto uživatelů." + "Zablokovali jste %1$d uživatelů. Nebudete dostávat oznámení od těchto uživatelů." + + "Blokovaní uživatelé" "Získat název aktuálního poskytovatele." "Nebyli vybráni žádní push poskytovatelé." "Aktuální push poskytovatel: %1$s." diff --git a/libraries/push/impl/src/main/res/values-da/translations.xml b/libraries/push/impl/src/main/res/values-da/translations.xml index facfc0eb3e..17893b5135 100644 --- a/libraries/push/impl/src/main/res/values-da/translations.xml +++ b/libraries/push/impl/src/main/res/values-da/translations.xml @@ -54,6 +54,14 @@ "Synkronisering i baggrunden" "Google-tjenester" "Der blev ikke fundet nogen gyldige Google Play-tjenester. Notifikationer fungerer muligvis ikke korrekt." + "Kontrollerer blokerede brugere" + "Se blokerede brugere" + "Ingen brugere er blokeret." + + "Du har blokeret %1$d bruger. Du vil ikke modtage meddelelser fra denne bruger." + "Du har blokeret %1$d brugere. Du vil ikke modtage meddelelser fra disse brugere." + + "Blokerede brugere" "Få navnet på den aktuelle udbyder." "Ingen push-udbydere valgt." "Nuværende push-udbyder: %1$s." 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 e2ef9109ba..81e1815168 100644 --- a/libraries/push/impl/src/main/res/values-de/translations.xml +++ b/libraries/push/impl/src/main/res/values-de/translations.xml @@ -15,14 +15,14 @@ "Du hast neue Nachrichten." "Eingehender Anruf" - "** Fehler beim Senden - bitte Raum öffnen" + "** Fehler beim Senden - bitte Chat öffnen" "Beitreten" "Ablehnen" "%d Einladung" "%d Einladungen" - "Sie wurden zu einem Chat eingeladen" + "Du wurdest zu einem Chat eingeladen" "%1$s hat dich zum Chatten eingeladen" "Hat Dich erwähnt: %1$s" "Neue Nachrichten" @@ -33,11 +33,11 @@ "Reagiert mit %1$s" "Als gelesen markieren" "Schnelle Antwort" - "Du wurdest eingeladen, den Raum zu betreten" - "%1$s hat dich eingeladen, dem Chatroom beizutreten" + "Du wurdest eingeladen, den Chat zu betreten" + "%1$s hat dich eingeladen, dem Chat beizutreten" "Ich" "%1$s hat Dich erwähnt oder geantwortet" - "Sie sehen die Benachrichtigung! Klicken Sie hier!" + "Du siehst dir die Benachrichtigung an! Klicke hier!" "%1$s: %2$s" "%1$s: %2$s %3$s" @@ -48,31 +48,39 @@ "%1$s in %2$s" "%1$s in %2$s und %3$s" - "%d Raum" - "%d Räume" + "%d Chat" + "%d Chats" "Hintergrundsynchronisation" "Google-Dienste" "Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig." + "Überprüfen von gesperrten Nutzern" + "Gesperrte Nutzer ansehen" + "Keine Nutzer sind gesperrt." + + "Du hast %1$d Nutzer gesperrt. Du wirst für diesen Nutzer keine Benachrichtigungen erhalten." + "Du hast %1$d Nutzer gesperrt. Du wirst für diese Nutzer keine Benachrichtigungen erhalten." + + "Gesperrte Nutzer" "Ermittele den Namen des aktuellen Anbieters." - "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." + "Kein Dienst für Push-Benachrichtigungen ausgewählt." + "Aktueller Push-Dienst: %1$s." + "Aktueller Push-Dienst" + "Stelle sicher, dass die Anwendung mindestens einen Push-Dienst hat." + "Keine Unterstützung für Push-Dienst gefunden." - "%1$d Push-Anbieter gefunden: %2$s" - "%1$d Push-Anbieter gefunden: %2$s" + "%1$d Push-Dienst gefunden: %2$s" + "%1$d Push-Dienst gefunden: %2$s" "Die Anwendung bietet Unterstützung für: %1$s" - "Unterstützung für Pushanbieter" + "Unterstützung für Push-Dienst" "Prüfe, ob die Anwendung Benachrichtigungen anzeigen kann." "Die Benachrichtigung wurde nicht angeklickt." "Die Benachrichtigung kann nicht angezeigt werden." "Die Benachrichtigung wurde angeklickt!" "Benachrichtigung anzeigen" "Bitte klicke auf die Benachrichtigung, um den Test fortzusetzen." - "Stelle sicher, dass die Anwendung Push-Nachrichten empfängt." + "Stelle sicher, dass die App Push-Benachrichtigungen empfängt." "Fehler: Der Pusher hat die Anfrage abgelehnt." "Fehler:%1$s." "Fehler: Push kann nicht getestet werden." 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 0484f0008c..b16f99a8a2 100644 --- a/libraries/push/impl/src/main/res/values-et/translations.xml +++ b/libraries/push/impl/src/main/res/values-et/translations.xml @@ -54,6 +54,14 @@ "Sünkroniseerimine taustal" "Google\'i teenused" "Google Play Services taustateenust ei leidu. Teavitused ei pruugi toimida korrektselt." + "Kontrollin blokeeritud kasutajaid" + "Vaata blokeeritud kasutajaid" + "Sa pole ühtegi kasutajat blokeerinud." + + "Sa oled blokeerinud %1$d kasutaja. Sa ei saa tema kohta teavitusi" + "Sa oled blokeerinud %1$d kasutajat. Sa ei saa tema kohta teavitusi" + + "Blokeeritud kasutajad" "Vali hetkel kasutatava tõuketeenuste pakkuja nimi." "Tõuketeenuste pakkujaid pole valitud." "Hetkel kasutatav tõuketeenuste pakkuja: %1$s." diff --git a/libraries/push/impl/src/main/res/values-eu/translations.xml b/libraries/push/impl/src/main/res/values-eu/translations.xml index f548b40c34..c72badbb52 100644 --- a/libraries/push/impl/src/main/res/values-eu/translations.xml +++ b/libraries/push/impl/src/main/res/values-eu/translations.xml @@ -12,6 +12,7 @@ "jakinarazpen %d" "%d jakinarazpen" + "Mezu berriak dituzu." "Deia jasotzen" "** Huts egin du bidalketak - ireki gela" "Elkartu" 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 0069c1e5a7..8bf165e79a 100644 --- a/libraries/push/impl/src/main/res/values-fr/translations.xml +++ b/libraries/push/impl/src/main/res/values-fr/translations.xml @@ -54,6 +54,14 @@ "Synchronisation en arrière-plan" "Services Google" "Aucun service Google Play valide n’a été trouvé. Les notifications peuvent ne pas fonctionner correctement." + "Vérification des utilisateurs bloqués" + "Voir les utilisateurs bloqués" + "Aucun utilisateur n’est bloqué." + + "Vous avez bloqué %1$d utilisateur. Vous ne recevrez pas de notifications pour cet utilisateur." + "Vous avez bloqué %1$d utilisateurs. Vous ne recevrez pas de notifications pour ces utilisateurs." + + "Utilisateurs bloqués" "Obtenir le nom du fournisseur de Push actuel." "Aucun fournisseur de Push n’est sélectionné." "Fournisseur de Push actuel : %1$s." 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 c0aad928d4..fb067af5c4 100644 --- a/libraries/push/impl/src/main/res/values-hu/translations.xml +++ b/libraries/push/impl/src/main/res/values-hu/translations.xml @@ -54,6 +54,14 @@ "Háttérszinkronizálás" "Google szolgáltatások" "A Google Play szolgáltatások nem találhatók. Előfordulhat, hogy az értesítések nem működnek megfelelően." + "Letiltott felhasználók ellenőrzése" + "Letiltott felhasználók megtekintése" + "Nincs felhasználó letiltva." + + "Letiltotta %1$d felhasználót. Nem fog értesítéseket kapni erről a felhasználóról." + "Letiltott %1$d felhasználót. Nem fog értesítéseket kapni ezekről a felhasználókról." + + "Letiltott felhasználók" "A jelenlegi szolgáltató nevének lekérdezése." "Nincs kiválasztva leküldéses értesítési szolgáltató." "Jelenlegi leküldéses értesítési szolgáltató: %1$s." @@ -61,8 +69,8 @@ "Győződjön meg arról, hogy az alkalmazás legalább egy leküldéses értesítési szolgáltatóval rendelkezik." "Nem található leküldéses értesítési szolgáltató." - "%1$d leküldéses szolgáltató találva: %2$s" - "%1$d leküldéses szolgáltató találva: %2$s" + "%1$d leküldéses értesítési szolgáltató találva: %2$s" + "%1$d leküldéses értesítési 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" @@ -78,5 +86,5 @@ "Hiba, nem lehet tesztelni a leküldéses értesítést." "Hiba, időtúllépés a leküldéses értesítésre való várakozás során." "A leküldéses értesítés folyamata %1$d ezredmásodpercig tartott." - "Tesztelje a leküldéses értesítés folyamatát" + "Leküldéses értesítés folyamatának tesztelése" 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 919d892440..07a85e9f42 100644 --- a/libraries/push/impl/src/main/res/values-it/translations.xml +++ b/libraries/push/impl/src/main/res/values-it/translations.xml @@ -36,7 +36,7 @@ "Ti ha invitato ad entrare nella stanza" "%1$s ti ha invitato a unirti alla stanza" "Io" - "%1$s menzionato o risposto" + "%1$s è stato menzionato o risposto" "Stai visualizzando la notifica! Cliccami!" "%1$s: %2$s" "%1$s: %2$s %3$s" diff --git a/libraries/push/impl/src/main/res/values-ko/translations.xml b/libraries/push/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..9c2aa8c710 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,75 @@ + + + "통화" + "이벤트 수신" + "소리 알림" + "전화벨이 울린다" + "무음 알림" + + "%1$s: %2$d 메세지" + + + "%d 알림" + + "알림" + "📹 수신 전화" + "** 전송 실패 - 방을 열여주세요" + "참가하기" + "거부" + + "%d 초대" + + "채팅에 초대됨" + "%1$s 가 채팅에 초대했습니다" + "당신을 언급했습니다: %1$s" + "새 메시지" + + "%d 새 메시지" + + "%1$s로 반응함" + "읽음으로 표시" + "빠른 답장" + "방에 초대받음" + "%1$s 가 당신을 이 방에 초대했습니다" + "나" + "%1$s 언급하거나 답변함" + "알림을 보고 있습니다! 클릭해주세요!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + + "%d 읽지 않은 메시지 알림" + + "%1$s 및 %2$s" + "%1$s 내 %2$s" + "%1$s 내 %2$s 및 %3$s" + + "%d 방" + + "백그라운드 동기화" + "Google 서비스" + "유효한 Google Play 서비스를 찾지 못했습니다. 알림이 정상적으로 동작하지 않을 수 있습니다." + "현재 제공자의 이름을 가져옵니다." + "푸시 제공자가 선택되지 않았습니다." + "현재 푸시 제공자: %1$s." + "현재 푸시 제공자" + "애플리케이션이 적어도 하나의 푸시 제공자를 지원하는지 확인하십시오." + "푸시 제공자 지원이 발견되지 않았습니다." + + "%1$d 푸시 제공자를 찾았습니다: %2$s" + + "이 애플리케이션은 다음을 지원하도록 구축되었습니다: %1$s" + "푸시 제공자 지원" + "애플리케이션에서 알림을 표시할 수 있는지 확인하세요." + "알림이 클릭되지 않았습니다." + "알림을 표시할 수 없습니다." + "알림이 클릭되었습니다!" + "알림 표시" + "알림을 클릭하여 테스트를 계속하세요." + "애플리케이션이 푸시를 수신하는지 확인하세요." + "오류: 푸셔가 요청을 거부했습니다." + "오류: %1$s." + "오류, 푸시 테스트가 불가능합니다." + "오류, 푸시 대기 중 시간 초과." + "푸시 루프백이 %1$d ms 소요되었습니다." + "테스트 푸시 루프백" + 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 fb577b0dd4..6d2e96d82c 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 @@ -31,7 +31,7 @@ "%d novas mensagens" "Reagiu com %1$s" - "Marcar como lido" + "Marcar como lida" "Resposta rápida" "Convidou você para entrar na sala" "%1$s te convidou para entrar na sala" @@ -58,14 +58,14 @@ "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." + "Certifique-se de que o aplicativo tenha suporte a pelo menos um provedor de push." + "Nenhum provedor de push com suporte foi encontrado." - "Encontrado %1$d provedor de push: %2$s" - "Encontrados %1$d provedores de push: %2$s" + "Foi encontrado %1$d provedor de push: %2$s" + "Foram encontrados %1$d provedores de push: %2$s" - "O aplicativo foi desenvolvido com suporte para: %1$s" - "Suporte ao provedor de push" + "O aplicativo foi compilado com suporte para: %1$s" + "Suporte a provedores 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." @@ -77,6 +77,6 @@ "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." + "O loopback do push 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 0a55a1dd7b..1d0184e516 100644 --- a/libraries/push/impl/src/main/res/values-pt/translations.xml +++ b/libraries/push/impl/src/main/res/values-pt/translations.xml @@ -23,7 +23,7 @@ "%d convites" "Convidou-te para conversar" - "%1$s convidou-o para conversar" + "%1$s convidou-te para conversar" "Mencionou-te: %1$s" "Mensagens novas" @@ -34,7 +34,7 @@ "Marcar como lida" "Resposta rápida" "Convidou-te a entrar na sala" - "%1$s convidou-o a juntar-se à sala" + "%1$s convidou-te a entrares na sala" "Eu" "%1$s mencionou ou respondeu" "Estás a ver a notificação! Clica em mim!" @@ -54,6 +54,14 @@ "Sincronização em segundo plano" "Serviços do Google Play" "Nenhuns Serviços do Google Play válidos encontrados. As notificações poderão não funcionar devidamente." + "A verificar utilizadores bloqueados" + "Ver utilizadores bloqueados" + "Sem utilizadores bloqueados." + + "Bloqueaste %1$d utilizador. Não receberás notificações dele." + "Bloqueaste %1$d utilizadores. Não receberás notificações deles." + + "Utilizadores bloqueados" "Obtém o nome do fornecedor atual." "Nenhum fornecedor de envio selecionado." "Fornecedor de envio atual: %1$s." diff --git a/libraries/push/impl/src/main/res/values-ro/translations.xml b/libraries/push/impl/src/main/res/values-ro/translations.xml index 14240a8d59..00986e689a 100644 --- a/libraries/push/impl/src/main/res/values-ro/translations.xml +++ b/libraries/push/impl/src/main/res/values-ro/translations.xml @@ -13,6 +13,7 @@ "%d notificare" "%d notificări" + "Aveți mesaje noi" "Apel primit" "** Trimiterea eșuată - vă rugăm să deschideți camera" "Alăturați-vă" @@ -64,6 +65,7 @@ "S-au găsit %1$d furnizori push: %2$s" "S-au găsit %1$d furnizori push: %2$s" + "Aplicația a fost creată cu suport pentru: %1$s" "Detectați furnizorii push" "Verificați dacă aplicația poate afișa notificări." "Notificarea nu a fost apăsată." 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 7345664ce0..1cb9873b4f 100644 --- a/libraries/push/impl/src/main/res/values-ru/translations.xml +++ b/libraries/push/impl/src/main/res/values-ru/translations.xml @@ -15,6 +15,7 @@ "%d уведомления" "%d уведомлений" + "У вас есть новые сообщения." "📹 Входящий вызов" "** Не удалось отправить - пожалуйста, откройте комнату" "Присоединиться" diff --git a/libraries/push/impl/src/main/res/values-tr/translations.xml b/libraries/push/impl/src/main/res/values-tr/translations.xml index 183950f3b9..a3c415a210 100644 --- a/libraries/push/impl/src/main/res/values-tr/translations.xml +++ b/libraries/push/impl/src/main/res/values-tr/translations.xml @@ -13,6 +13,7 @@ "%d bildirim" "%d bildirim" + "Yeni mesajlarınız var" "📹 Gelen çağrı" "** Gönderilemedi - lütfen odayı açın" "Katıl" 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 dd6797660d..4a9e4bffbc 100644 --- a/libraries/push/impl/src/main/res/values-uz/translations.xml +++ b/libraries/push/impl/src/main/res/values-uz/translations.xml @@ -3,6 +3,7 @@ "Qo\'ng\'iroq" "Voqealarni tinglash" "Shovqinli bildirishnomalar" + "Jiringlayotgan qoʻngʻiroqlar" "Ovozsiz bildirishnomalar" "%1$s:%2$d xabar" @@ -13,6 +14,7 @@ "%dbildirishnomalar" "Sizda yangi xabarlar bor." + "📹 Kiruvchi qoʻngʻiroq" "** Yuborilmadi - iltimos, xonani oching" "Qo\'shilish" "Rad etish" @@ -21,15 +23,20 @@ "%dtaklifnomalar" "Sizni suhbatga taklif qildi" + "%1$s sizni suhbatga taklif qildi" + "%1$s sizni eslatib oʻtdi" "Yangi xabarlar" "%dyangi xabar" "%dyangi xabarlar" "%1$sbilan munosabat bildiring" + "Oʻqilgan deb belgilash" "Tez javob" "Sizni xonaga kirishga taklif qildi" + "%1$s sizni xonaga kirishga taklif qildi" "Men" + "%1$s eslatib o‘tdi yoki javob qaytardi" "Siz bildirishnomani ko\'ryapsiz! Meni bosing!" "%1$s:%2$s" "%1$s:%2$s%3$s" @@ -47,4 +54,26 @@ "Orqa Fon sinxronizatsiyasi" "Google xizmatlari" "Yaroqli Google Play xizmatlari topilmadi. Bildirishnomalar to\'g\'ri ishlamasligi mumkin." + "Joriy provayder nomini oling." + "Hech qanday push-provayder tanlanmagan." + "Joriy push provider: %1$s." + "Joriy push provider" + "Ilova kamida bitta push-provayderni qo‘llab-quvvatlashini tekshiring." + "Hech qanday push-provayder xizmati topilmadi." + + "Topildi %1$d push provider: %2$s" + "Topildi %1$d push provayderlar: %2$s" + + "Provayderni qoʻllab-quvvatlash" + "Ilova bildirishnomani koʻrsata olishini tekshiring." + "Bildirishnoma bosilmagan." + "Bildirishnomani ko‘rsatib boʻlmaydi." + "Bildirishnoma bosildi!" + "Bildirishnomani koʻrsatish" + "Sinovni davom ettirish uchun bildirishnoma ustiga bosing." + "Ilovaning push-bildirishnomalarni qabul qilayotganiga ishonch hosil qiling." + "Xato: pusher so‘rovni rad etdi." + "Xato: %1$s." + "Xatolik, push qilishni sinab bo‘lmadi." + "Xatolik, taym aut pushni kutmoqda." 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 677551bb0c..b4e7bfb08c 100644 --- a/libraries/push/impl/src/main/res/values-zh/translations.xml +++ b/libraries/push/impl/src/main/res/values-zh/translations.xml @@ -11,6 +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 069183a3a2..8e9da34b73 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -54,6 +54,14 @@ "Background synchronization" "Google Services" "No valid Google Play Services found. Notifications may not work properly." + "Checking blocked users" + "View blocked users" + "No users are blocked." + + "You blocked %1$d user. You will not receive notifications for this user." + "You blocked %1$d users. You will not receive notifications for these users." + + "Blocked users" "Get the name of the current provider." "No push providers selected." "Current push provider: %1$s." 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 index c9ebbb72b3..f9097752f1 100644 --- 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 @@ -8,8 +8,8 @@ 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.api.notification.RtcNotificationType 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 @@ -61,11 +61,12 @@ class DefaultCallNotificationEventResolverTest { isUpdated = false, senderDisambiguatedDisplayName = A_USER_NAME_2, senderAvatarUrl = null, - callNotifyType = CallNotifyType.RING, + expirationTimestamp = 1567L, + rtcNotificationType = RtcNotificationType.RING, ) val notificationData = aNotificationData( - content = NotificationContent.MessageLike.CallNotify(A_USER_ID_2, CallNotifyType.RING) + content = NotificationContent.MessageLike.RtcNotification(A_USER_ID_2, RtcNotificationType.RING, 1567) ) val result = resolver.resolveEvent(A_SESSION_ID, notificationData) assertThat(result.getOrNull()).isEqualTo(expectedResult) @@ -105,11 +106,11 @@ class DefaultCallNotificationEventResolverTest { imageUriString = null, imageMimeType = null, threadId = null, - type = "m.call.notify", + type = "org.matrix.msc4075.rtc.notification", ) val notificationData = aNotificationData( - content = NotificationContent.MessageLike.CallNotify(A_USER_ID_2, CallNotifyType.NOTIFY) + content = NotificationContent.MessageLike.RtcNotification(A_USER_ID_2, RtcNotificationType.NOTIFY, 0) ) val result = resolver.resolveEvent(A_SESSION_ID, notificationData) assertThat(result.getOrNull()).isEqualTo(expectedResult) @@ -149,11 +150,11 @@ class DefaultCallNotificationEventResolverTest { imageUriString = null, imageMimeType = null, threadId = null, - type = "m.call.notify", + type = "org.matrix.msc4075.rtc.notification", ) val notificationData = aNotificationData( - content = NotificationContent.MessageLike.CallNotify(A_USER_ID_2, CallNotifyType.RING) + content = NotificationContent.MessageLike.RtcNotification(A_USER_ID_2, RtcNotificationType.RING, 0) ) val result = resolver.resolveEvent(A_SESSION_ID, notificationData) assertThat(result.getOrNull()).isEqualTo(expectedResult) 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 2d99e31d74..25e6b92b16 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 @@ -12,9 +12,9 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.exception.NotificationResolverException 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 import io.element.android.libraries.matrix.api.notification.NotificationData +import io.element.android.libraries.matrix.api.notification.RtcNotificationType import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType @@ -693,9 +693,10 @@ class DefaultNotifiableEventResolverTest { notificationResult = Result.success( mapOf( AN_EVENT_ID to Result.success(aNotificationData( - content = NotificationContent.MessageLike.CallNotify( + content = NotificationContent.MessageLike.RtcNotification( A_USER_ID_2, - CallNotifyType.NOTIFY + RtcNotificationType.NOTIFY, + 0 ), )) ) @@ -719,7 +720,7 @@ class DefaultNotifiableEventResolverTest { isRedacted = false, imageUriString = null, imageMimeType = null, - type = EventType.CALL_NOTIFY, + type = EventType.RTC_NOTIFICATION, ) ) callNotificationEventResolver.resolveEventLambda = { _, _, _ -> Result.success(expectedResult.notifiableEvent) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationServiceTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationServiceTest.kt index 92adfd7812..d05966f9a6 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationServiceTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationServiceTest.kt @@ -21,6 +21,7 @@ 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.FakeMatrixClientProvider import io.element.android.libraries.push.impl.notifications.factories.FakeIntentProvider +import io.element.android.libraries.push.impl.notifications.shortcut.createShortcutId import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver @@ -58,7 +59,7 @@ class DefaultNotificationConversationServiceTest { val context = InstrumentationRegistry.getInstrumentation().context val service = createService(context) - val shortcutId = "$A_SESSION_ID-$A_ROOM_ID" + val shortcutId = createShortcutId(A_SESSION_ID, A_ROOM_ID) val shortcutInfo = ShortcutInfoCompat.Builder(context, shortcutId) .setShortLabel("Room title") .setIntent(Intent(Intent.ACTION_VIEW)) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt index b926806bcd..0d466d90fd 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt @@ -12,7 +12,7 @@ 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.notification.CallNotifyType +import io.element.android.libraries.matrix.api.notification.RtcNotificationType import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EVENT_ID @@ -119,8 +119,9 @@ fun aNotifiableCallEvent( senderName: String? = null, roomAvatarUrl: String? = AN_AVATAR_URL, senderAvatarUrl: String? = AN_AVATAR_URL, - callNotifyType: CallNotifyType = CallNotifyType.NOTIFY, + rtcNotificationType: RtcNotificationType = RtcNotificationType.NOTIFY, timestamp: Long = 0L, + expirationTimestamp: Long = 0L, ) = NotifiableRingingCallEvent( sessionId = sessionId, eventId = eventId, @@ -129,6 +130,7 @@ fun aNotifiableCallEvent( editedEventId = null, description = "description", timestamp = timestamp, + expirationTimestamp = expirationTimestamp, canBeReplaced = false, isRedacted = false, isUpdated = false, @@ -136,5 +138,5 @@ fun aNotifiableCallEvent( senderId = senderId, roomAvatarUrl = roomAvatarUrl, senderAvatarUrl = senderAvatarUrl, - callNotifyType = callNotifyType, + rtcNotificationType = rtcNotificationType, ) 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 a070156d05..e482385b3d 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 @@ -14,13 +14,12 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.call.api.CallType import io.element.android.features.call.test.FakeElementCallEntryPoint import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService 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.exception.NotificationResolverException -import io.element.android.libraries.matrix.api.notification.CallNotifyType +import io.element.android.libraries.matrix.api.notification.RtcNotificationType import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 @@ -28,7 +27,6 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SECRET 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 @@ -181,7 +179,7 @@ class DefaultPushHandlerTest { } @Test - fun `when PushData is received, but client secret is not known, fallback the latest session`() = + fun `when PushData is received, but client secret is not known, nothing happen`() = runTest { val aNotifiableMessageEvent = aNotifiableMessageEvent() val notifiableEventResult = @@ -207,58 +205,6 @@ class DefaultPushHandlerTest { pushClientSecret = FakePushClientSecret( getUserIdFromSecretResult = { null } ), - matrixAuthenticationService = FakeMatrixAuthenticationService().apply { - getLatestSessionIdLambda = { A_USER_ID } - }, - incrementPushCounterResult = incrementPushCounterResult, - pushHistoryService = pushHistoryService, - ) - defaultPushHandler.handle(aPushData, A_PUSHER_INFO) - - advanceTimeBy(300.milliseconds) - - incrementPushCounterResult.assertions() - .isCalledOnce() - notifiableEventResult.assertions() - .isCalledOnce() - .with(value(A_USER_ID), any()) - onNotifiableEventsReceived.assertions() - .isCalledOnce() - .with(value(listOf(aNotifiableMessageEvent))) - onPushReceivedResult.assertions() - .isCalledOnce() - } - - @Test - fun `when PushData is received, but client secret is not known, and there is no latest session, nothing happen`() = - 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 aPushData = PushData( - eventId = AN_EVENT_ID, - roomId = A_ROOM_ID, - unread = 0, - clientSecret = A_SECRET, - ) - val onPushReceivedResult = lambdaRecorder { _, _, _, _, _, _, _ -> } - val pushHistoryService = FakePushHistoryService( - onPushReceivedResult = onPushReceivedResult, - ) - val defaultPushHandler = createDefaultPushHandler( - onNotifiableEventsReceived = onNotifiableEventsReceived, - notifiableEventsResult = notifiableEventResult, - pushClientSecret = FakePushClientSecret( - getUserIdFromSecretResult = { null } - ), - matrixAuthenticationService = FakeMatrixAuthenticationService().apply { - getLatestSessionIdLambda = { null } - }, incrementPushCounterResult = incrementPushCounterResult, pushHistoryService = pushHistoryService, ) @@ -388,7 +334,7 @@ class DefaultPushHandlerTest { mapOf( request to Result.success( ResolvedPushEvent.Event( - aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()) + aNotifiableCallEvent(rtcNotificationType = RtcNotificationType.RING, timestamp = Instant.now().toEpochMilli()) ) ) ) @@ -440,7 +386,7 @@ class DefaultPushHandlerTest { 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))))) + Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.RTC_NOTIFICATION))))) }, incrementPushCounterResult = {}, pushClientSecret = FakePushClientSecret( @@ -655,8 +601,8 @@ class DefaultPushHandlerTest { var receivedFallbackEvent = false val onPushReceivedResult = lambdaRecorder { _, _, _, _, isResolved, _, comment -> - receivedFallbackEvent = !isResolved && comment == "Unable to resolve event: ${aNotifiableFallbackEvent.cause}" - } + receivedFallbackEvent = !isResolved && comment == "Unable to resolve event: ${aNotifiableFallbackEvent.cause}" + } val pushHistoryService = FakePushHistoryService( onPushReceivedResult = onPushReceivedResult, ) @@ -694,7 +640,6 @@ class DefaultPushHandlerTest { userPushStore: UserPushStore = FakeUserPushStore(), pushClientSecret: PushClientSecret = FakePushClientSecret(), buildMeta: BuildMeta = aBuildMeta(), - matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), diagnosticPushHandler: DiagnosticPushHandler = DiagnosticPushHandler(), elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(), notificationChannels: FakeNotificationChannels = FakeNotificationChannels(), @@ -712,7 +657,6 @@ class DefaultPushHandlerTest { userPushStoreFactory = FakeUserPushStoreFactory { userPushStore }, pushClientSecret = pushClientSecret, buildMeta = buildMeta, - matrixAuthenticationService = matrixAuthenticationService, diagnosticPushHandler = diagnosticPushHandler, elementCallEntryPoint = elementCallEntryPoint, notificationChannels = notificationChannels, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/CurrentPushProviderTestTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/CurrentPushProviderTestTest.kt index f6485aa7d6..95bfced7de 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/CurrentPushProviderTestTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/CurrentPushProviderTestTest.kt @@ -7,12 +7,11 @@ package io.element.android.libraries.push.impl.troubleshoot -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.push.test.FakeGetCurrentPushProvider import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.test.runAndTestState import io.element.android.services.toolbox.test.strings.FakeStringProvider -import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Test @@ -23,10 +22,7 @@ class CurrentPushProviderTestTest { getCurrentPushProvider = FakeGetCurrentPushProvider("foo"), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() @@ -41,14 +37,11 @@ class CurrentPushProviderTestTest { getCurrentPushProvider = FakeGetCurrentPushProvider(null), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure()) sut.reset() assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/IgnoredUsersTestTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/IgnoredUsersTestTest.kt new file mode 100644 index 0000000000..e38baa7321 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/IgnoredUsersTestTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: 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.troubleshoot + +import com.google.common.truth.Truth.assertThat +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.troubleshoot.api.test.NotificationTroubleshootNavigator +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.test.runAndTestState +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class IgnoredUsersTestTest { + @Test + fun `test IgnoredUsersTest order`() = runTest { + val sut = IgnoredUsersTest( + matrixClient = FakeMatrixClient(), + stringProvider = FakeStringProvider(), + ) + assertThat(sut.order).isEqualTo(80) + } + + @Test + fun `test IgnoredUsersTest quick fix`() = runTest { + val sut = IgnoredUsersTest( + matrixClient = FakeMatrixClient(), + stringProvider = FakeStringProvider(), + ) + val openIgnoredUsersResult = lambdaRecorder {} + val navigator = object : NotificationTroubleshootNavigator { + override fun openIgnoredUsers() = openIgnoredUsersResult() + } + sut.quickFix( + coroutineScope = backgroundScope, + navigator = navigator, + ) + openIgnoredUsersResult.assertions().isCalledOnce() + } + + @Test + fun `test IgnoredUsersTest with no blocked users`() = runTest { + val sut = IgnoredUsersTest( + matrixClient = FakeMatrixClient( + ignoredUsersFlow = MutableStateFlow(persistentListOf()) + ), + stringProvider = FakeStringProvider(), + ) + sut.runAndTestState { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success) + } + } + + @Test + fun `test IgnoredUsersTest with blocked users`() = runTest { + val sut = IgnoredUsersTest( + matrixClient = FakeMatrixClient( + ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID, A_USER_ID_2)) + ), + stringProvider = FakeStringProvider(), + ) + sut.runAndTestState { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + val lastStatus = lastItem.status as NotificationTroubleshootTestState.Status.Failure + assertThat(lastStatus.hasQuickFix).isTrue() + assertThat(lastStatus.isCritical).isFalse() + assertThat(lastStatus.quickFixButtonString).isNotNull() + assertThat(lastItem.description).contains("2") + } + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTestTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTestTest.kt index 2c0dbeff10..eecb3801fd 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTestTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTestTest.kt @@ -7,14 +7,13 @@ package io.element.android.libraries.push.impl.troubleshoot -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.test.runAndTestState import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.tests.testutils.lambda.lambdaRecorder -import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Test @@ -31,10 +30,7 @@ class NotificationTestTest { fun `test NotificationTest notification cannot be displayed`() = runTest { fakeNotificationDisplayer.displayDiagnosticNotificationResult = lambdaRecorder { _ -> false } val sut = createNotificationTest() - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isInstanceOf(NotificationTroubleshootTestState.Status.Failure::class.java) @@ -44,10 +40,7 @@ class NotificationTestTest { @Test fun `test NotificationTest user does not click on notification`() = runTest { val sut = createNotificationTest() - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.WaitingForUser) @@ -60,10 +53,7 @@ class NotificationTestTest { @Test fun `test NotificationTest user clicks on notification`() = runTest { val sut = createNotificationTest() - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.WaitingForUser) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTestTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTestTest.kt index e4fa26a28b..f06b6a9e1b 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTestTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTestTest.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.push.impl.troubleshoot -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_FAILURE_REASON @@ -15,10 +14,11 @@ import io.element.android.libraries.push.api.gateway.PushGatewayFailure import io.element.android.libraries.push.test.FakePushService import io.element.android.libraries.pushproviders.test.FakePushProvider import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator +import io.element.android.libraries.troubleshoot.test.runAndTestState import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.lambdaRecorder -import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Test @@ -32,14 +32,11 @@ class PushLoopbackTestTest { clock = FakeSystemClock(), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure()) } } @@ -56,14 +53,11 @@ class PushLoopbackTestTest { clock = FakeSystemClock(), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure()) sut.reset() assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) } @@ -89,17 +83,14 @@ class PushLoopbackTestTest { clock = FakeSystemClock(), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) - sut.quickFix(this) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true)) + sut.quickFix(this, FakeNotificationTroubleshootNavigator()) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) - assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true)) rotateTokenLambda.assertions().isCalledOnce() } } @@ -115,14 +106,11 @@ class PushLoopbackTestTest { clock = FakeSystemClock(), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure()) } } @@ -139,14 +127,11 @@ class PushLoopbackTestTest { clock = FakeSystemClock(), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure()) assertThat(lastItem.description).contains(A_FAILURE_REASON) } } @@ -163,10 +148,7 @@ class PushLoopbackTestTest { clock = FakeSystemClock(), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() 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 8947b646a6..b274f0bdab 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 @@ -7,12 +7,11 @@ package io.element.android.libraries.push.impl.troubleshoot -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.pushproviders.test.FakePushProvider import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.test.runAndTestState import io.element.android.services.toolbox.test.strings.FakeStringProvider -import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Test @@ -23,14 +22,11 @@ class PushProvidersTestTest { pushProviders = emptySet(), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure()) sut.reset() assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) } @@ -45,10 +41,7 @@ class PushProvidersTestTest { ), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushData.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushData.kt index e08ab1c18f..6000f74a95 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushData.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushData.kt @@ -22,5 +22,5 @@ data class PushData( val eventId: EventId, val roomId: RoomId, val unread: Int?, - val clientSecret: String?, + val clientSecret: String, ) diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index 445efec8ee..05e60920e5 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -8,7 +8,8 @@ @file:Suppress("UnstableApiUsage") import config.BuildTimeConfig -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies plugins { id("io.element.android-library") @@ -45,10 +46,9 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { - implementation(libs.dagger) implementation(libs.androidx.corektx) implementation(projects.features.enterprise.api) implementation(projects.libraries.architecture) @@ -69,18 +69,13 @@ dependencies { exclude(group = "com.google.firebase", module = "firebase-measurement-connector") } - testImplementation(libs.coroutines.test) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs) 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) - testImplementation(projects.libraries.sessionStorage.implMemory) testImplementation(projects.libraries.sessionStorage.test) - testImplementation(projects.tests.testutils) + testImplementation(projects.libraries.troubleshoot.test) testImplementation(projects.services.toolbox.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 index 2b378766bc..a872808e16 100644 --- 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 @@ -7,17 +7,18 @@ package io.element.android.libraries.pushproviders.firebase -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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( +@Inject +class DefaultFirebaseGatewayProvider( private val enterpriseService: EnterpriseService, ) : FirebaseGatewayProvider { override fun getFirebaseGateway(): String { 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 e5589af493..2853d52685 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 @@ -7,10 +7,11 @@ package io.element.android.libraries.pushproviders.firebase -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.core.log.logger.LoggerTag -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.pushproviders.api.PusherSubscriber @@ -18,7 +19,6 @@ import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.toUserList import timber.log.Timber -import javax.inject.Inject private val loggerTag = LoggerTag("FirebaseNewTokenHandler", LoggerTag.PushLoggerTag) @@ -30,7 +30,8 @@ interface FirebaseNewTokenHandler { } @ContributesBinding(AppScope::class) -class DefaultFirebaseNewTokenHandler @Inject constructor( +@Inject +class DefaultFirebaseNewTokenHandler( private val pusherSubscriber: PusherSubscriber, private val sessionStore: SessionStore, private val userPushStoreFactory: UserPushStoreFactory, diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushParser.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushParser.kt index d142c71e6f..410fc05014 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushParser.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushParser.kt @@ -7,10 +7,11 @@ package io.element.android.libraries.pushproviders.firebase +import dev.zacsweers.metro.Inject import io.element.android.libraries.pushproviders.api.PushData -import javax.inject.Inject -class FirebasePushParser @Inject constructor() { +@Inject +class FirebasePushParser { fun parse(message: Map): PushData? { val pushDataFirebase = PushDataFirebase( eventId = message["event_id"], 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 a6be83e803..806eb98bf5 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 @@ -7,9 +7,10 @@ package io.element.android.libraries.pushproviders.firebase -import com.squareup.anvil.annotations.ContributesMultibinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.log.logger.LoggerTag -import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig @@ -17,12 +18,12 @@ import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushproviders.api.PusherSubscriber import timber.log.Timber -import javax.inject.Inject private val loggerTag = LoggerTag("FirebasePushProvider", LoggerTag.PushLoggerTag) -@ContributesMultibinding(AppScope::class) -class FirebasePushProvider @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class FirebasePushProvider( private val firebaseStore: FirebaseStore, private val pusherSubscriber: PusherSubscriber, private val isPlayServiceAvailable: IsPlayServiceAvailable, diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt index 8f1c26ccb4..91fec3bb57 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt @@ -9,13 +9,13 @@ package io.element.android.libraries.pushproviders.firebase import android.content.SharedPreferences import androidx.core.content.edit -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart -import javax.inject.Inject /** * This class store the Firebase token in SharedPrefs. @@ -27,7 +27,8 @@ interface FirebaseStore { } @ContributesBinding(AppScope::class) -class SharedPreferencesFirebaseStore @Inject constructor( +@Inject +class SharedPreferencesFirebaseStore( private val sharedPreferences: SharedPreferences, ) : FirebaseStore { override fun getFcmToken(): String? { diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenDeleter.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenDeleter.kt index c3aa66d3e4..4a8be152ad 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenDeleter.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenDeleter.kt @@ -8,10 +8,10 @@ package io.element.android.libraries.pushproviders.firebase import com.google.firebase.messaging.FirebaseMessaging -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import timber.log.Timber -import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -24,7 +24,8 @@ interface FirebaseTokenDeleter { } @ContributesBinding(AppScope::class) -class DefaultFirebaseTokenDeleter @Inject constructor( +@Inject +class DefaultFirebaseTokenDeleter( private val isPlayServiceAvailable: IsPlayServiceAvailable, ) : FirebaseTokenDeleter { override suspend fun delete() { diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenGetter.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenGetter.kt index 9e76dd9f52..4add5e4f8b 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenGetter.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenGetter.kt @@ -8,10 +8,10 @@ package io.element.android.libraries.pushproviders.firebase import com.google.firebase.messaging.FirebaseMessaging -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import timber.log.Timber -import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -25,7 +25,8 @@ interface FirebaseTokenGetter { } @ContributesBinding(AppScope::class) -class DefaultFirebaseTokenGetter @Inject constructor( +@Inject +class DefaultFirebaseTokenGetter( private val isPlayServiceAvailable: IsPlayServiceAvailable, ) : FirebaseTokenGetter { override suspend fun get(): String { 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 cf6eb4b2cc..c54221aa38 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 @@ -7,10 +7,10 @@ package io.element.android.libraries.pushproviders.firebase -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.extensions.runCatchingExceptions -import io.element.android.libraries.di.AppScope -import javax.inject.Inject interface FirebaseTokenRotator { suspend fun rotate(): Result @@ -20,7 +20,8 @@ interface FirebaseTokenRotator { * This class delete the Firebase token and generate a new one. */ @ContributesBinding(AppScope::class) -class DefaultFirebaseTokenRotator @Inject constructor( +@Inject +class DefaultFirebaseTokenRotator( private val firebaseTokenDeleter: FirebaseTokenDeleter, private val firebaseTokenGetter: FirebaseTokenGetter, ) : FirebaseTokenRotator { 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 8326496dd2..132996ee34 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 @@ -7,10 +7,10 @@ package io.element.android.libraries.pushproviders.firebase -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.extensions.runCatchingExceptions -import io.element.android.libraries.di.AppScope -import javax.inject.Inject interface FirebaseTroubleshooter { suspend fun troubleshoot(): Result @@ -20,7 +20,8 @@ interface FirebaseTroubleshooter { * This class force retrieving and storage of the Firebase token. */ @ContributesBinding(AppScope::class) -class DefaultFirebaseTroubleshooter @Inject constructor( +@Inject +class DefaultFirebaseTroubleshooter( private val newTokenHandler: FirebaseNewTokenHandler, private val firebaseTokenGetter: FirebaseTokenGetter, ) : FirebaseTroubleshooter { diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/IsPlayServiceAvailable.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/IsPlayServiceAvailable.kt index ddaaad6bc2..8e25407a91 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/IsPlayServiceAvailable.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/IsPlayServiceAvailable.kt @@ -10,11 +10,11 @@ package io.element.android.libraries.pushproviders.firebase import android.content.Context import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailabilityLight -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext import timber.log.Timber -import javax.inject.Inject interface IsPlayServiceAvailable { fun isAvailable(): Boolean @@ -27,7 +27,8 @@ fun IsPlayServiceAvailable.checkAvailableOrThrow() { } @ContributesBinding(AppScope::class) -class DefaultIsPlayServiceAvailable @Inject constructor( +@Inject +class DefaultIsPlayServiceAvailable( @ApplicationContext private val context: Context, ) : IsPlayServiceAvailable { override fun isAvailable(): Boolean { diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/PushDataFirebase.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/PushDataFirebase.kt index 7bc1515332..fb33ab9c1f 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/PushDataFirebase.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/PushDataFirebase.kt @@ -34,10 +34,11 @@ data class PushDataFirebase( fun PushDataFirebase.toPushData(): PushData? { val safeEventId = eventId?.let(::EventId) ?: return null val safeRoomId = roomId?.let(::RoomId) ?: return null + val safeClientSecret = clientSecret ?: return null return PushData( eventId = safeEventId, roomId = safeRoomId, unread = unread, - clientSecret = clientSecret, + clientSecret = safeClientSecret, ) } 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 2c138c3cb6..6d13143ddc 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 @@ -9,6 +9,7 @@ package io.element.android.libraries.pushproviders.firebase import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.annotations.AppCoroutineScope @@ -16,7 +17,6 @@ import io.element.android.libraries.pushproviders.api.PushHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject private val loggerTag = LoggerTag("VectorFirebaseMessagingService", LoggerTag.PushLoggerTag) @@ -29,7 +29,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { override fun onCreate() { super.onCreate() - applicationContext.bindings().inject(this) + bindings().inject(this) } override fun onNewToken(token: String) { diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceBindings.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceBindings.kt index c679c88366..0f3e109fef 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceBindings.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceBindings.kt @@ -7,8 +7,8 @@ package io.element.android.libraries.pushproviders.firebase -import com.squareup.anvil.annotations.ContributesTo -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesTo @ContributesTo(AppScope::class) interface VectorFirebaseMessagingServiceBindings { diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTest.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTest.kt index 0a5ccd82c5..c4ffe19db3 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTest.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTest.kt @@ -7,8 +7,9 @@ package io.element.android.libraries.pushproviders.firebase.troubleshoot -import com.squareup.anvil.annotations.ContributesMultibinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.libraries.pushproviders.firebase.FirebaseConfig import io.element.android.libraries.pushproviders.firebase.IsPlayServiceAvailable import io.element.android.libraries.pushproviders.firebase.R @@ -19,10 +20,10 @@ import io.element.android.libraries.troubleshoot.api.test.TestFilterData import io.element.android.services.toolbox.api.strings.StringProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject -@ContributesMultibinding(AppScope::class) -class FirebaseAvailabilityTest @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class FirebaseAvailabilityTest( private val isPlayServiceAvailable: IsPlayServiceAvailable, private val stringProvider: StringProvider, ) : NotificationTroubleshootTest { @@ -50,7 +51,7 @@ class FirebaseAvailabilityTest @Inject constructor( } else { delegate.updateState( description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_availability_failure), - status = NotificationTroubleshootTestState.Status.Failure(false) + status = NotificationTroubleshootTestState.Status.Failure() ) } } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt index 12e1762a73..a886b5fb77 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt @@ -7,12 +7,14 @@ package io.element.android.libraries.pushproviders.firebase.troubleshoot -import com.squareup.anvil.annotations.ContributesMultibinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.libraries.pushproviders.firebase.FirebaseConfig import io.element.android.libraries.pushproviders.firebase.FirebaseStore import io.element.android.libraries.pushproviders.firebase.FirebaseTroubleshooter import io.element.android.libraries.pushproviders.firebase.R +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState @@ -23,10 +25,10 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import javax.inject.Inject -@ContributesMultibinding(AppScope::class) -class FirebaseTokenTest @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class FirebaseTokenTest( private val firebaseStore: FirebaseStore, private val firebaseTroubleshooter: FirebaseTroubleshooter, private val stringProvider: StringProvider, @@ -61,7 +63,7 @@ class FirebaseTokenTest @Inject constructor( } else { delegate.updateState( description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_token_failure), - status = NotificationTroubleshootTestState.Status.Failure(true) + status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true) ) } } @@ -70,7 +72,10 @@ class FirebaseTokenTest @Inject constructor( override suspend fun reset() = delegate.reset() - override suspend fun quickFix(coroutineScope: CoroutineScope) { + override suspend fun quickFix( + coroutineScope: CoroutineScope, + navigator: NotificationTroubleshootNavigator, + ) { delegate.start() firebaseTroubleshooter.troubleshoot() run(coroutineScope) diff --git a/libraries/pushproviders/firebase/src/main/res/values-bg/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..7efaff4545 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-bg/translations.xml @@ -0,0 +1,7 @@ + + + "Уверете се, че Firebase е наличен." + "Firebase не е наличен." + "Firebase е наличен." + "Проверка на Firebase" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-ko/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..3521f4fb48 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-ko/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-pt-rBR/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-pt-rBR/translations.xml index 3d7dcf4dd4..51b5091106 100644 --- a/libraries/pushproviders/firebase/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/pushproviders/firebase/src/main/res/values-pt-rBR/translations.xml @@ -3,9 +3,9 @@ "Certifique-se de que o Firebase esteja disponível." "O Firebase não está disponível." "O Firebase está disponível." - "Verifique o Firebase" + "Verificar 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" + "Verificar o token do Firebase" diff --git a/libraries/pushproviders/firebase/src/main/res/values-uz/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-uz/translations.xml new file mode 100644 index 0000000000..0a2a1cf9d2 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-uz/translations.xml @@ -0,0 +1,11 @@ + + + "Firebase mavjudligiga ishonch hosil qiling." + "Firebase mavjud emas." + "Firebase mavjud." + "Firebase-ni tekshiring" + "Firebase tokeni mavjudligiga ishonch hosil qiling." + "Firebase mavjudligiga ishonch hosil qiling." + "Firebase tokeni: %1$s ." + "Firebase tokenini tekshiring" + 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 c8c5dfa9bf..70cd24e6c8 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 @@ -22,8 +22,7 @@ import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory import io.element.android.libraries.sessionstorage.api.SessionStore -import io.element.android.libraries.sessionstorage.impl.memory.InMemoryMultiSessionsStore -import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -50,11 +49,13 @@ class DefaultFirebaseNewTokenHandlerTest { val registerPusherResult = lambdaRecorder> { _, _, _ -> Result.success(Unit) } val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult) val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler( - sessionStore = InMemoryMultiSessionsStore().apply { - storeData(aSessionData(A_USER_ID.value)) - storeData(aSessionData(A_USER_ID_2.value)) - storeData(aSessionData(A_USER_ID_3.value)) - }, + sessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData(A_USER_ID.value), + aSessionData(A_USER_ID_2.value), + aSessionData(A_USER_ID_3.value), + ) + ), matrixClientProvider = FakeMatrixClientProvider { sessionId -> when (sessionId) { A_USER_ID -> Result.success(aMatrixClient1) @@ -89,9 +90,9 @@ class DefaultFirebaseNewTokenHandlerTest { val registerPusherResult = lambdaRecorder> { _, _, _ -> Result.success(Unit) } val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult) val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler( - sessionStore = InMemoryMultiSessionsStore().apply { - storeData(aSessionData(A_USER_ID.value)) - }, + sessionStore = InMemorySessionStore( + initialList = listOf(aSessionData(A_USER_ID.value)) + ), matrixClientProvider = FakeMatrixClientProvider { Result.failure(IllegalStateException()) }, @@ -113,9 +114,9 @@ class DefaultFirebaseNewTokenHandlerTest { val registerPusherResult = lambdaRecorder> { _, _, _ -> Result.failure(AN_EXCEPTION) } val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult) val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler( - sessionStore = InMemoryMultiSessionsStore().apply { - storeData(aSessionData(A_USER_ID.value)) - }, + sessionStore = InMemorySessionStore( + initialList = listOf(aSessionData(A_USER_ID.value)) + ), matrixClientProvider = FakeMatrixClientProvider { Result.success(aMatrixClient1) }, diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushParserTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushParserTest.kt index 71848fc4df..49ed5bc6d4 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushParserTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushParserTest.kt @@ -59,6 +59,12 @@ class FirebasePushParserTest { assertThrowsInDebug { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "")) } } + @Test + fun `test empty client secret`() { + val pushParser = FirebasePushParser() + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("cs", null))).isNull() + } + @Test fun `test invalid eventId`() { val pushParser = FirebasePushParser() diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTestTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTestTest.kt index 86afded560..ce617799f5 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTestTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTestTest.kt @@ -7,14 +7,13 @@ package io.element.android.libraries.pushproviders.firebase.troubleshoot -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.pushproviders.firebase.FakeIsPlayServiceAvailable import io.element.android.libraries.pushproviders.firebase.FirebaseConfig import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.TestFilterData +import io.element.android.libraries.troubleshoot.test.runAndTestState import io.element.android.services.toolbox.test.strings.FakeStringProvider -import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Test @@ -25,10 +24,7 @@ class FirebaseAvailabilityTestTest { isPlayServiceAvailable = FakeIsPlayServiceAvailable(true), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() @@ -42,14 +38,11 @@ class FirebaseAvailabilityTestTest { isPlayServiceAvailable = FakeIsPlayServiceAvailable(false), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure()) sut.reset() assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) } diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt index b66f094d76..7e6981ef57 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt @@ -7,15 +7,15 @@ package io.element.android.libraries.pushproviders.firebase.troubleshoot -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.pushproviders.firebase.FakeFirebaseTroubleshooter import io.element.android.libraries.pushproviders.firebase.FirebaseConfig import io.element.android.libraries.pushproviders.firebase.InMemoryFirebaseStore import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.TestFilterData +import io.element.android.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator +import io.element.android.libraries.troubleshoot.test.runAndTestState import io.element.android.services.toolbox.test.strings.FakeStringProvider -import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Test @@ -27,10 +27,7 @@ class FirebaseTokenTestTest { firebaseTroubleshooter = FakeFirebaseTroubleshooter(), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() @@ -53,16 +50,13 @@ class FirebaseTokenTestTest { ), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true)) // Quick fix - sut.quickFix(this) + sut.quickFix(this, FakeNotificationTroubleshootNavigator()) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success) } @@ -81,13 +75,10 @@ class FirebaseTokenTestTest { ), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) - assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true)) sut.reset() assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) } diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts index c98d98f64e..e416ccf8bf 100644 --- a/libraries/pushproviders/unifiedpush/build.gradle.kts +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -15,10 +16,9 @@ android { namespace = "io.element.android.libraries.pushproviders.unifiedpush" } -setupAnvil() +setupDependencyInjection() dependencies { - implementation(libs.dagger) implementation(projects.features.enterprise.api) implementation(projects.libraries.androidutils) implementation(projects.libraries.core) @@ -42,19 +42,16 @@ dependencies { implementation(libs.serialization.json) // UnifiedPush library - implementation(libs.unifiedpush) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.junit) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + api(libs.unifiedpush) + + testCommonDependencies(libs) 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) testImplementation(projects.libraries.pushstore.test) - testImplementation(projects.tests.testutils) + testImplementation(projects.libraries.troubleshoot.test) testImplementation(projects.services.toolbox.test) testImplementation(projects.services.appnavstate.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 index af434c3020..d330b2b7fe 100644 --- 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 @@ -7,17 +7,18 @@ package io.element.android.libraries.pushproviders.unifiedpush -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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( +@Inject +class DefaultDefaultPushGatewayHttpUrlProvider( private val enterpriseService: EnterpriseService, ) : DefaultPushGatewayHttpUrlProvider { override fun provide(): String { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/GuardServiceStarter.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/GuardServiceStarter.kt index 4072cc387d..57813f8fde 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/GuardServiceStarter.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/GuardServiceStarter.kt @@ -7,9 +7,9 @@ package io.element.android.libraries.pushproviders.unifiedpush -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import javax.inject.Inject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject interface GuardServiceStarter { fun start() {} @@ -17,4 +17,5 @@ interface GuardServiceStarter { } @ContributesBinding(AppScope::class) -class NoopGuardServiceStarter @Inject constructor() : GuardServiceStarter +@Inject +class NoopGuardServiceStarter : GuardServiceStarter 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 862212c333..2f300f891d 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 @@ -8,16 +8,16 @@ package io.element.android.libraries.pushproviders.unifiedpush import android.content.Context -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeout import org.unifiedpush.android.connector.UnifiedPush -import javax.inject.Inject import kotlin.time.Duration.Companion.seconds interface RegisterUnifiedPushUseCase { @@ -25,7 +25,8 @@ interface RegisterUnifiedPushUseCase { } @ContributesBinding(AppScope::class) -class DefaultRegisterUnifiedPushUseCase @Inject constructor( +@Inject +class DefaultRegisterUnifiedPushUseCase( @ApplicationContext private val context: Context, private val endpointRegistrationHandler: EndpointRegistrationHandler, ) : RegisterUnifiedPushUseCase { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushApiFactory.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushApiFactory.kt index 6b2aefe6f1..6427b6bd00 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushApiFactory.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushApiFactory.kt @@ -7,18 +7,19 @@ package io.element.android.libraries.pushproviders.unifiedpush -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.network.RetrofitFactory import io.element.android.libraries.pushproviders.unifiedpush.network.UnifiedPushApi -import javax.inject.Inject interface UnifiedPushApiFactory { fun create(baseUrl: String): UnifiedPushApi } @ContributesBinding(AppScope::class) -class DefaultUnifiedPushApiFactory @Inject constructor( +@Inject +class DefaultUnifiedPushApiFactory( private val retrofitFactory: RetrofitFactory, ) : UnifiedPushApiFactory { override fun create(baseUrl: String): UnifiedPushApi { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushCurrentUserPushConfigProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushCurrentUserPushConfigProvider.kt index b48393c0eb..ba7301e010 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushCurrentUserPushConfigProvider.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushCurrentUserPushConfigProvider.kt @@ -7,20 +7,21 @@ package io.element.android.libraries.pushproviders.unifiedpush -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.currentSessionId -import javax.inject.Inject interface UnifiedPushCurrentUserPushConfigProvider { suspend fun provide(): CurrentUserPushConfig? } @ContributesBinding(AppScope::class) -class DefaultUnifiedPushCurrentUserPushConfigProvider @Inject constructor( +@Inject +class DefaultUnifiedPushCurrentUserPushConfigProvider( private val pushClientSecret: PushClientSecret, private val unifiedPushStore: UnifiedPushStore, private val appNavigationStateService: AppNavigationStateService, diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushDistributorProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushDistributorProvider.kt index 92746c62c8..aad8d9cd0b 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushDistributorProvider.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushDistributorProvider.kt @@ -8,20 +8,21 @@ package io.element.android.libraries.pushproviders.unifiedpush import android.content.Context -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.system.getApplicationLabel -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.pushproviders.api.Distributor import org.unifiedpush.android.connector.UnifiedPush -import javax.inject.Inject interface UnifiedPushDistributorProvider { fun getDistributors(): List } @ContributesBinding(AppScope::class) -class DefaultUnifiedPushDistributorProvider @Inject constructor( +@Inject +class DefaultUnifiedPushDistributorProvider( @ApplicationContext private val context: Context, ) : UnifiedPushDistributorProvider { override fun getDistributors(): List { 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 8dc54dff8c..1aa6b5d436 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 @@ -7,17 +7,17 @@ package io.element.android.libraries.pushproviders.unifiedpush -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 import timber.log.Timber import java.net.HttpURLConnection import java.net.URL -import javax.inject.Inject sealed interface UnifiedPushGatewayResolverResult { data class Success(val gateway: String) : UnifiedPushGatewayResolverResult @@ -33,7 +33,8 @@ interface UnifiedPushGatewayResolver { private val loggerTag = LoggerTag("DefaultUnifiedPushGatewayResolver") @ContributesBinding(AppScope::class) -class DefaultUnifiedPushGatewayResolver @Inject constructor( +@Inject +class DefaultUnifiedPushGatewayResolver( private val unifiedPushApiFactory: UnifiedPushApiFactory, private val coroutineDispatchers: CoroutineDispatchers, ) : UnifiedPushGatewayResolver { 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 e7e31cfd67..48f8153787 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 @@ -7,9 +7,9 @@ package io.element.android.libraries.pushproviders.unifiedpush -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import javax.inject.Inject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject interface UnifiedPushGatewayUrlResolver { fun resolve( @@ -19,7 +19,8 @@ interface UnifiedPushGatewayUrlResolver { } @ContributesBinding(AppScope::class) -class DefaultUnifiedPushGatewayUrlResolver @Inject constructor( +@Inject +class DefaultUnifiedPushGatewayUrlResolver( private val unifiedPushStore: UnifiedPushStore, private val defaultPushGatewayHttpUrlProvider: DefaultPushGatewayHttpUrlProvider, ) : UnifiedPushGatewayUrlResolver { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt index 3436a028c2..a899c0787f 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt @@ -7,16 +7,16 @@ package io.element.android.libraries.pushproviders.unifiedpush -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.core.log.logger.LoggerTag -import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.pushproviders.api.PusherSubscriber import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import timber.log.Timber -import javax.inject.Inject private val loggerTag = LoggerTag("DefaultUnifiedPushNewGatewayHandler", LoggerTag.PushLoggerTag) @@ -28,7 +28,8 @@ interface UnifiedPushNewGatewayHandler { } @ContributesBinding(AppScope::class) -class DefaultUnifiedPushNewGatewayHandler @Inject constructor( +@Inject +class DefaultUnifiedPushNewGatewayHandler( private val pusherSubscriber: PusherSubscriber, private val userPushStoreFactory: UserPushStoreFactory, private val pushClientSecret: PushClientSecret, diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushParser.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushParser.kt index 16b1d72d5a..f1290636c3 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushParser.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushParser.kt @@ -7,12 +7,13 @@ package io.element.android.libraries.pushproviders.unifiedpush +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.pushproviders.api.PushData import kotlinx.serialization.json.Json -import javax.inject.Inject -class UnifiedPushParser @Inject constructor() { +@Inject +class UnifiedPushParser { private val json by lazy { Json { ignoreUnknownKeys = true } } fun parse(message: ByteArray, clientSecret: String): PushData? { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProvider.kt index 4eabf8f29f..03b754e26f 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProvider.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProvider.kt @@ -7,18 +7,19 @@ package io.element.android.libraries.pushproviders.unifiedpush -import com.squareup.anvil.annotations.ContributesMultibinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret -import javax.inject.Inject -@ContributesMultibinding(AppScope::class) -class UnifiedPushProvider @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class UnifiedPushProvider( private val unifiedPushDistributorProvider: UnifiedPushDistributorProvider, private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, private val unRegisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushStore.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushStore.kt index fac161814c..96c92b09d8 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushStore.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushStore.kt @@ -10,11 +10,11 @@ package io.element.android.libraries.pushproviders.unifiedpush import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.core.UserId -import javax.inject.Inject interface UnifiedPushStore { fun getEndpoint(clientSecret: String): String? @@ -26,7 +26,8 @@ interface UnifiedPushStore { } @ContributesBinding(AppScope::class) -class SharedPreferencesUnifiedPushStore @Inject constructor( +@Inject +class SharedPreferencesUnifiedPushStore( @ApplicationContext val context: Context, private val sharedPreferences: SharedPreferences, ) : UnifiedPushStore { 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 24b9676c11..429c23d747 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 @@ -8,14 +8,14 @@ package io.element.android.libraries.pushproviders.unifiedpush import android.content.Context -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.pushproviders.api.PusherSubscriber import org.unifiedpush.android.connector.UnifiedPush import timber.log.Timber -import javax.inject.Inject interface UnregisterUnifiedPushUseCase { /** @@ -30,7 +30,8 @@ interface UnregisterUnifiedPushUseCase { } @ContributesBinding(AppScope::class) -class DefaultUnregisterUnifiedPushUseCase @Inject constructor( +@Inject +class DefaultUnregisterUnifiedPushUseCase( @ApplicationContext private val context: Context, private val unifiedPushStore: UnifiedPushStore, private val pusherSubscriber: PusherSubscriber, 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 b19aa985fe..97ef0b27f1 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 @@ -9,6 +9,7 @@ package io.element.android.libraries.pushproviders.unifiedpush import android.content.Context import android.content.Intent +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.annotations.AppCoroutineScope @@ -22,7 +23,6 @@ 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 private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver", LoggerTag.PushLoggerTag) @@ -39,7 +39,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { @Inject lateinit var coroutineScope: CoroutineScope override fun onReceive(context: Context, intent: Intent) { - context.applicationContext.bindings().inject(this) + context.bindings().inject(this) super.onReceive(context, intent) } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt index f686419778..bbec3f272c 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt @@ -7,10 +7,15 @@ package io.element.android.libraries.pushproviders.unifiedpush -import com.squareup.anvil.annotations.ContributesTo -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo +import org.unifiedpush.android.connector.MessagingReceiver @ContributesTo(AppScope::class) interface VectorUnifiedPushMessagingReceiverBindings { fun inject(receiver: VectorUnifiedPushMessagingReceiver) + + @Binds + fun bindsMessagingReceiver(vectorUnifiedPushMessagingReceiver: VectorUnifiedPushMessagingReceiver): MessagingReceiver } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/registration/EndpointRegistrationHandler.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/registration/EndpointRegistrationHandler.kt index a3ec295155..c6a1353a2f 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/registration/EndpointRegistrationHandler.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/registration/EndpointRegistrationHandler.kt @@ -7,11 +7,11 @@ package io.element.android.libraries.pushproviders.unifiedpush.registration -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow -import javax.inject.Inject data class RegistrationResult( val clientSecret: String, @@ -19,7 +19,8 @@ data class RegistrationResult( ) @SingleIn(AppScope::class) -class EndpointRegistrationHandler @Inject constructor() { +@Inject +class EndpointRegistrationHandler { private val _state = MutableSharedFlow() val state: SharedFlow = _state diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/OpenDistributorWebPageAction.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/OpenDistributorWebPageAction.kt index 3e1afb86e3..5f4eb4538c 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/OpenDistributorWebPageAction.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/OpenDistributorWebPageAction.kt @@ -8,19 +8,20 @@ package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot import android.content.Context -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.system.openUrlInExternalApp -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig -import javax.inject.Inject interface OpenDistributorWebPageAction { fun execute() } @ContributesBinding(AppScope::class) -class DefaultOpenDistributorWebPageAction @Inject constructor( +@Inject +class DefaultOpenDistributorWebPageAction( @ApplicationContext private val context: Context, ) : OpenDistributorWebPageAction { override fun execute() { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushMatrixGatewayTest.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushMatrixGatewayTest.kt index 8290452177..7a22e92222 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushMatrixGatewayTest.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushMatrixGatewayTest.kt @@ -7,9 +7,10 @@ package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot -import com.squareup.anvil.annotations.ContributesMultibinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.di.AppScope import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushApiFactory import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushCurrentUserPushConfigProvider @@ -20,10 +21,10 @@ import io.element.android.libraries.troubleshoot.api.test.TestFilterData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import javax.inject.Inject -@ContributesMultibinding(AppScope::class) -class UnifiedPushMatrixGatewayTest @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class UnifiedPushMatrixGatewayTest( private val unifiedPushApiFactory: UnifiedPushApiFactory, private val coroutineDispatchers: CoroutineDispatchers, private val unifiedPushCurrentUserPushConfigProvider: UnifiedPushCurrentUserPushConfigProvider, @@ -47,7 +48,7 @@ class UnifiedPushMatrixGatewayTest @Inject constructor( if (config == null) { delegate.updateState( description = "No current push provider", - status = NotificationTroubleshootTestState.Status.Failure(false) + status = NotificationTroubleshootTestState.Status.Failure() ) } else { val gatewayBaseUrl = config.url.removeSuffix("/_matrix/push/v1/notify") @@ -64,13 +65,13 @@ class UnifiedPushMatrixGatewayTest @Inject constructor( } else { delegate.updateState( description = "${config.url} is not a Matrix gateway.", - status = NotificationTroubleshootTestState.Status.Failure(false) + status = NotificationTroubleshootTestState.Status.Failure() ) } } catch (throwable: Throwable) { delegate.updateState( description = "Fail to check the gateway ${config.url}: ${throwable.localizedMessage}", - status = NotificationTroubleshootTestState.Status.Failure(false) + status = NotificationTroubleshootTestState.Status.Failure() ) } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTest.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTest.kt index d2718ee9a5..bb2c88bcdd 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTest.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTest.kt @@ -7,11 +7,13 @@ package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot -import com.squareup.anvil.annotations.ContributesMultibinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import io.element.android.libraries.pushproviders.unifiedpush.R import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushDistributorProvider +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState @@ -19,10 +21,10 @@ import io.element.android.libraries.troubleshoot.api.test.TestFilterData import io.element.android.services.toolbox.api.strings.StringProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject -@ContributesMultibinding(AppScope::class) -class UnifiedPushTest @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class UnifiedPushTest( private val unifiedPushDistributorProvider: UnifiedPushDistributorProvider, private val openDistributorWebPageAction: OpenDistributorWebPageAction, private val stringProvider: StringProvider, @@ -56,14 +58,17 @@ class UnifiedPushTest @Inject constructor( } else { delegate.updateState( description = stringProvider.getString(R.string.troubleshoot_notifications_test_unified_push_failure), - status = NotificationTroubleshootTestState.Status.Failure(true) + status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true) ) } } override suspend fun reset() = delegate.reset() - override suspend fun quickFix(coroutineScope: CoroutineScope) { + override suspend fun quickFix( + coroutineScope: CoroutineScope, + navigator: NotificationTroubleshootNavigator, + ) { openDistributorWebPageAction.execute() } } diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-ko/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..ca789ffd57 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-ko/translations.xml @@ -0,0 +1,9 @@ + + + "UnifiedPush 배포자가 사용할 수 있는지 확인하세요." + "푸시 배포자가 발견되지 않았습니다." + + "%1$d 배포자 목록: %2$s." + + "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 index 4f7a8726ce..beaaf54360 100644 --- a/libraries/pushproviders/unifiedpush/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-pt-rBR/translations.xml @@ -4,7 +4,7 @@ "Nenhum distribuidor push encontrado." "%1$d distribuidor encontrado: %2$s." - "%1$d Distribuidores encontrados: %2$s." + "%1$d distribuidores encontrados: %2$s." - "Verifique o UnifiedPush" + "Verificar o UnifiedPush" diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-uz/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-uz/translations.xml new file mode 100644 index 0000000000..a1d978c144 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-uz/translations.xml @@ -0,0 +1,10 @@ + + + "UnifiedPush distribyutorlari mavjudligiga ishonch hosil qiling." + "Push distribyutorlari topilmadi." + + "%1$d ta distribyutor topildi: %2$s." + "%1$d ta distribyutor topildi: %2$s." + + "UnifiedPush tekshiruvi" + diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushMatrixGatewayTestTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushMatrixGatewayTestTest.kt index 95cf85af5c..f4a0cf5da4 100644 --- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushMatrixGatewayTestTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushMatrixGatewayTestTest.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig import io.element.android.libraries.pushproviders.test.aCurrentUserPushConfig @@ -18,8 +17,8 @@ import io.element.android.libraries.pushproviders.unifiedpush.matrixDiscoveryRes import io.element.android.libraries.pushproviders.unifiedpush.network.DiscoveryResponse import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.TestFilterData +import io.element.android.libraries.troubleshoot.test.runAndTestState import io.element.android.tests.testutils.testCoroutineDispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -31,10 +30,7 @@ class UnifiedPushMatrixGatewayTestTest { currentUserPushConfig = aCurrentUserPushConfig(), discoveryResponse = matrixDiscoveryResponse, ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() @@ -48,14 +44,11 @@ class UnifiedPushMatrixGatewayTestTest { currentUserPushConfig = null, discoveryResponse = matrixDiscoveryResponse, ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure()) } } @@ -65,14 +58,11 @@ class UnifiedPushMatrixGatewayTestTest { currentUserPushConfig = aCurrentUserPushConfig(), discoveryResponse = invalidDiscoveryResponse, ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure()) // Reset the error sut.reset() assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) @@ -85,14 +75,11 @@ class UnifiedPushMatrixGatewayTestTest { currentUserPushConfig = aCurrentUserPushConfig(), discoveryResponse = { error("Network error") }, ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure()) } } diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTestTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTestTest.kt index 7b9059a324..28ab0186da 100644 --- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTestTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTestTest.kt @@ -7,12 +7,13 @@ package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.TestFilterData +import io.element.android.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator +import io.element.android.libraries.troubleshoot.test.runAndTestState import io.element.android.services.toolbox.test.strings.FakeStringProvider import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest @@ -30,10 +31,7 @@ class UnifiedPushTestTest { openDistributorWebPageAction = FakeOpenDistributorWebPageAction(), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() @@ -57,17 +55,14 @@ class UnifiedPushTestTest { ), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true)) // Quick fix - launch { - sut.quickFix(this) + backgroundScope.launch { + sut.quickFix(this, FakeNotificationTroubleshootNavigator()) sut.run(this) } assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) @@ -91,14 +86,11 @@ class UnifiedPushTestTest { ), stringProvider = FakeStringProvider(), ) - launch { - sut.run(this) - } - sut.state.test { + sut.runAndTestState { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() - assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true)) sut.reset() assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) } diff --git a/libraries/pushstore/impl/build.gradle.kts b/libraries/pushstore/impl/build.gradle.kts index ec81643cfd..ea45836d1f 100644 --- a/libraries/pushstore/impl/build.gradle.kts +++ b/libraries/pushstore/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -19,25 +20,21 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { - implementation(libs.dagger) implementation(projects.libraries.architecture) implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.preferences.api) implementation(projects.libraries.pushstore.api) implementation(libs.androidx.corektx) implementation(libs.androidx.datastore.preferences) - testImplementation(libs.test.junit) - testImplementation(libs.test.mockk) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.coroutines.test) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.preferences.test) testImplementation(projects.services.appnavstate.test) testImplementation(projects.libraries.pushstore.test) diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt index 8003449c95..41dbe5c212 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt @@ -8,20 +8,23 @@ package io.element.android.libraries.pushstore.impl import android.content.Context -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.SingleIn +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import io.element.android.libraries.pushstore.api.UserPushStore import io.element.android.libraries.pushstore.api.UserPushStoreFactory import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultUserPushStoreFactory @Inject constructor( +@Inject +class DefaultUserPushStoreFactory( @ApplicationContext private val context: Context, + private val preferenceDataStoreFactory: PreferenceDataStoreFactory, ) : UserPushStoreFactory { // We can have only one class accessing a single data store, so keep a cache of them. private val cache = ConcurrentHashMap() @@ -29,7 +32,8 @@ class DefaultUserPushStoreFactory @Inject constructor( return cache.getOrPut(userId) { UserPushStoreDataStore( context = context, - userId = userId + userId = userId, + factory = preferenceDataStoreFactory, ) } } diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt index 1ab555bba3..399b9648f4 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt @@ -13,12 +13,12 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStoreFile import io.element.android.libraries.androidutils.hash.hash import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import io.element.android.libraries.pushstore.api.UserPushStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @@ -31,6 +31,7 @@ import timber.log.Timber class UserPushStoreDataStore( private val context: Context, userId: SessionId, + factory: PreferenceDataStoreFactory, ) : UserPushStore { // Hash the sessionId to get rid of exotic chars and take only the first 16 chars. // The risk of collision is not high. @@ -49,28 +50,28 @@ class UserPushStoreDataStore( } } - private val Context.dataStore: DataStore by preferencesDataStore(name = preferenceName) + private val store: DataStore = factory.create(preferenceName) private val pushProviderName = stringPreferencesKey("pushProviderName") private val currentPushKey = stringPreferencesKey("currentPushKey") private val notificationEnabled = booleanPreferencesKey("notificationEnabled") private val ignoreRegistrationError = booleanPreferencesKey("ignoreRegistrationError") override suspend fun getPushProviderName(): String? { - return context.dataStore.data.first()[pushProviderName] + return store.data.first()[pushProviderName] } override suspend fun setPushProviderName(value: String) { - context.dataStore.edit { + store.edit { it[pushProviderName] = value } } override suspend fun getCurrentRegisteredPushKey(): String? { - return context.dataStore.data.first()[currentPushKey] + return store.data.first()[currentPushKey] } override suspend fun setCurrentRegisteredPushKey(value: String?) { - context.dataStore.edit { + store.edit { if (value == null) { it.remove(currentPushKey) } else { @@ -80,11 +81,11 @@ class UserPushStoreDataStore( } override fun getNotificationEnabledForDevice(): Flow { - return context.dataStore.data.map { it[notificationEnabled].orTrue() } + return store.data.map { it[notificationEnabled].orTrue() } } override suspend fun setNotificationEnabledForDevice(enabled: Boolean) { - context.dataStore.edit { + store.edit { it[notificationEnabled] = enabled } } @@ -94,17 +95,17 @@ class UserPushStoreDataStore( } override fun ignoreRegistrationError(): Flow { - return context.dataStore.data.map { it[ignoreRegistrationError].orFalse() } + return store.data.map { it[ignoreRegistrationError].orFalse() } } override suspend fun setIgnoreRegistrationError(ignore: Boolean) { - context.dataStore.edit { + store.edit { it[ignoreRegistrationError] = ignore } } override suspend fun reset() { - context.dataStore.edit { + store.edit { it.clear() } // Also delete the file diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DataStorePushClientSecretStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DataStorePushClientSecretStore.kt index 6a8cbd543a..98ab23c9ea 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DataStorePushClientSecretStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DataStorePushClientSecretStore.kt @@ -7,44 +7,41 @@ package io.element.android.libraries.pushstore.impl.clientsecret -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences 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.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore import kotlinx.coroutines.flow.first -import javax.inject.Inject - -private val Context.dataStore: DataStore by preferencesDataStore(name = "push_client_secret_store") @ContributesBinding(AppScope::class) -class DataStorePushClientSecretStore @Inject constructor( - @ApplicationContext private val context: Context, +@Inject +class DataStorePushClientSecretStore( + preferenceDataStoreFactory: PreferenceDataStoreFactory, ) : PushClientSecretStore { + private val dataStore = preferenceDataStoreFactory.create("push_client_secret_store") + override suspend fun storeSecret(userId: SessionId, clientSecret: String) { - context.dataStore.edit { settings -> + dataStore.edit { settings -> settings[getPreferenceKeyForUser(userId)] = clientSecret } } override suspend fun getSecret(userId: SessionId): String? { - return context.dataStore.data.first()[getPreferenceKeyForUser(userId)] + return dataStore.data.first()[getPreferenceKeyForUser(userId)] } override suspend fun resetSecret(userId: SessionId) { - context.dataStore.edit { settings -> + dataStore.edit { settings -> settings.remove(getPreferenceKeyForUser(userId)) } } override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? { - val keyValues = context.dataStore.data.first().asMap() + val keyValues = dataStore.data.first().asMap() val matchingKey = keyValues.keys.find { keyValues[it] == clientSecret } diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DefaultPushClientSecret.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DefaultPushClientSecret.kt index 84ae182161..7125a7c122 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DefaultPushClientSecret.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DefaultPushClientSecret.kt @@ -7,16 +7,17 @@ package io.element.android.libraries.pushstore.impl.clientsecret -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultPushClientSecret @Inject constructor( +@Inject +class DefaultPushClientSecret( private val pushClientSecretFactory: PushClientSecretFactory, private val pushClientSecretStore: PushClientSecretStore, ) : PushClientSecret { diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DefaultPushClientSecretFactory.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DefaultPushClientSecretFactory.kt index 21d6f048f6..be991ca0c5 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DefaultPushClientSecretFactory.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DefaultPushClientSecretFactory.kt @@ -7,14 +7,15 @@ package io.element.android.libraries.pushstore.impl.clientsecret -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory import java.util.UUID -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultPushClientSecretFactory @Inject constructor() : PushClientSecretFactory { +@Inject +class DefaultPushClientSecretFactory : PushClientSecretFactory { override fun create(): String { return UUID.randomUUID().toString() } diff --git a/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStoreTest.kt b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStoreTest.kt index 1ba668073a..2c1a10a333 100644 --- a/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStoreTest.kt +++ b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStoreTest.kt @@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 +import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Test @@ -92,5 +93,6 @@ class UserPushStoreDataStoreTest { ) = UserPushStoreDataStore( context = InstrumentationRegistry.getInstrumentation().context, userId = sessionId, + factory = FakePreferenceDataStoreFactory(), ) } diff --git a/libraries/roomselect/impl/build.gradle.kts b/libraries/roomselect/impl/build.gradle.kts index 3ded043610..de0834e635 100644 --- a/libraries/roomselect/impl/build.gradle.kts +++ b/libraries/roomselect/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -16,7 +17,7 @@ android { namespace = "io.element.android.libraries.roomselect.impl" } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.core) @@ -28,11 +29,6 @@ dependencies { implementation(projects.libraries.uiStrings) api(projects.libraries.roomselect.api) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.tests.testutils) } diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPoint.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPoint.kt index 613e59848a..0c6ba57b9f 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPoint.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPoint.kt @@ -10,14 +10,15 @@ package io.element.android.libraries.roomselect.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 dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultRoomSelectEntryPoint @Inject constructor() : RoomSelectEntryPoint { +@Inject +class DefaultRoomSelectEntryPoint : RoomSelectEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomSelectEntryPoint.NodeBuilder { val plugins = ArrayList() diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectNode.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectNode.kt index 746501490f..a7e6dc0cb0 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectNode.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectNode.kt @@ -12,9 +12,9 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope @@ -23,7 +23,8 @@ import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint import io.element.android.libraries.roomselect.api.RoomSelectMode @ContributesNode(SessionScope::class) -class RoomSelectNode @AssistedInject constructor( +@AssistedInject +class RoomSelectNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: RoomSelectPresenter.Factory, diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt index a6bfb6d06b..ffd3fda5c0 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt @@ -16,9 +16,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.ui.model.SelectRoomInfo @@ -27,12 +27,13 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -class RoomSelectPresenter @AssistedInject constructor( +@AssistedInject +class RoomSelectPresenter( @Assisted private val mode: RoomSelectMode, private val dataSource: RoomSelectSearchDataSource, ) : Presenter { @AssistedFactory - interface Factory { + fun interface Factory { fun create(mode: RoomSelectMode): RoomSelectPresenter } diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectSearchDataSource.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectSearchDataSource.kt index 88a3201b62..58ee601548 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectSearchDataSource.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectSearchDataSource.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.roomselect.impl +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.roomlist.RoomList @@ -21,7 +22,6 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import javax.inject.Inject private const val PAGE_SIZE = 30 @@ -29,7 +29,8 @@ private const val PAGE_SIZE = 30 * DataSource for RoomSummaryDetails that can be filtered by a search query, * and which only includes rooms the user has joined. */ -class RoomSelectSearchDataSource @Inject constructor( +@Inject +class RoomSelectSearchDataSource( roomListService: RoomListService, coroutineDispatchers: CoroutineDispatchers, ) { diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPointTest.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPointTest.kt new file mode 100644 index 0000000000..bb9c15e7e6 --- /dev/null +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPointTest.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.android.libraries.roomselect.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint +import io.element.android.libraries.roomselect.api.RoomSelectMode +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class DefaultRoomSelectEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() = runTest { + val entryPoint = DefaultRoomSelectEntryPoint() + val testMode = RoomSelectMode.Share + val parentNode = TestParentNode.create { buildContext, plugins -> + RoomSelectNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { mode -> + assertThat(mode).isEqualTo(testMode) + createRoomSelectPresenter(mode) + }, + ) + } + val callback = object : RoomSelectEntryPoint.Callback { + override fun onRoomSelected(roomIds: List) = lambdaError() + override fun onCancel() = lambdaError() + } + val params = RoomSelectEntryPoint.Params(testMode) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .callback(callback) + .build() + assertThat(result).isInstanceOf(RoomSelectNode::class.java) + assertThat(result.plugins).contains(RoomSelectNode.Inputs(params.mode)) + assertThat(result.plugins).contains(callback) + } +} diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt index e8a8e11ca3..8294c97fad 100644 --- a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt @@ -111,15 +111,15 @@ class RoomSelectPresenterTest { cancel() } } - - private fun TestScope.createRoomSelectPresenter( - mode: RoomSelectMode = RoomSelectMode.Forward, - roomListService: RoomListService = FakeRoomListService(), - ) = RoomSelectPresenter( - mode = mode, - dataSource = RoomSelectSearchDataSource( - roomListService = roomListService, - coroutineDispatchers = testCoroutineDispatchers(), - ), - ) } + +internal fun TestScope.createRoomSelectPresenter( + mode: RoomSelectMode = RoomSelectMode.Forward, + roomListService: RoomListService = FakeRoomListService(), +) = RoomSelectPresenter( + mode = mode, + dataSource = RoomSelectSearchDataSource( + roomListService = roomListService, + coroutineDispatchers = testCoroutineDispatchers(), + ), +) diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt index c33c0d8861..90b0a0b7c7 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt @@ -39,4 +39,12 @@ data class SessionData( val sessionPath: String, /** The path to the cache data stored for the session in the filesystem. */ val cachePath: String, + /** The position, to be able to order account. */ + val position: Long, + /** The index of the last date of session usage. */ + val lastUsageIndex: Long, + /** The optional display name of the user. */ + val userDisplayName: String?, + /** The optional avatar URL of the user. */ + val userAvatarUrl: String?, ) diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt index a503ec0c28..9d9f143e15 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt @@ -11,18 +11,58 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map interface SessionStore { - fun isLoggedIn(): Flow + /** + * A flow emitting the current logged in state. + * If there is at least one session, the state is [LoggedInState.LoggedIn] with the latest used session. + * If there is no session, the state is [LoggedInState.NotLoggedIn]. + */ + fun loggedInStateFlow(): Flow + + /** + * Return a flow of all sessions ordered by last usage descending. + */ fun sessionsFlow(): Flow> - suspend fun storeData(sessionData: SessionData) + + /** + * Add a new session. If other sessions exist, the new one will be set as the latest used one, and + * the added session position will be set to a value higher than the other session positions. + */ + suspend fun addSession(sessionData: SessionData) /** * Will update the session data matching the userId, except the value of loginTimestamp. * No op if userId is not found in DB. */ suspend fun updateData(sessionData: SessionData) + + /** + * Update the user profile info of the session matching the userId. + */ + suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?) + + /** + * Get the session data matching the userId, or null if not found. + */ suspend fun getSession(sessionId: String): SessionData? + + /** + * Get all sessions ordered by last usage descending. + */ suspend fun getAllSessions(): List + + /** + * Get the latest session, or null if no session exists. + */ suspend fun getLatestSession(): SessionData? + + /** + * Set the session with [sessionId] as the latest used one. + */ + suspend fun setLatestSession(sessionId: String) + + /** + * Remove the session matching the sessionId. + */ suspend fun removeSession(sessionId: String) } diff --git a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemoryMultiSessionsStore.kt b/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemoryMultiSessionsStore.kt deleted file mode 100644 index 3782bfb625..0000000000 --- a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemoryMultiSessionsStore.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.libraries.sessionstorage.impl.memory - -import io.element.android.libraries.sessionstorage.api.LoggedInState -import io.element.android.libraries.sessionstorage.api.SessionData -import io.element.android.libraries.sessionstorage.api.SessionStore -import kotlinx.coroutines.flow.Flow - -class InMemoryMultiSessionsStore : SessionStore { - private val sessions = mutableListOf() - - override fun isLoggedIn(): Flow = error("Not implemented") - - override fun sessionsFlow(): Flow> = error("Not implemented") - - override suspend fun storeData(sessionData: SessionData) { - sessions.add(sessionData) - } - - override suspend fun updateData(sessionData: SessionData) = error("Not implemented") - - override suspend fun getSession(sessionId: String): SessionData? = error("Not implemented") - - override suspend fun getAllSessions(): List = sessions - - override suspend fun getLatestSession(): SessionData = error("Not implemented") - - override suspend fun removeSession(sessionId: String) = error("Not implemented") -} diff --git a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt b/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt deleted file mode 100644 index 09f05d256e..0000000000 --- a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.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.libraries.sessionstorage.impl.memory - -import io.element.android.libraries.sessionstorage.api.LoggedInState -import io.element.android.libraries.sessionstorage.api.SessionData -import io.element.android.libraries.sessionstorage.api.SessionStore -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map - -class InMemorySessionStore : SessionStore { - private var sessionDataFlow = MutableStateFlow(null) - - override fun isLoggedIn(): Flow { - return sessionDataFlow.map { - if (it == null) { - LoggedInState.NotLoggedIn - } else { - LoggedInState.LoggedIn( - sessionId = it.userId, - isTokenValid = it.isTokenValid, - ) - } - } - } - - override fun sessionsFlow(): Flow> { - return sessionDataFlow.map { listOfNotNull(it) } - } - - override suspend fun storeData(sessionData: SessionData) { - sessionDataFlow.value = sessionData - } - - override suspend fun updateData(sessionData: SessionData) { - sessionDataFlow.value = sessionData - } - - override suspend fun getSession(sessionId: String): SessionData? { - return sessionDataFlow.value.takeIf { it?.userId == sessionId } - } - - override suspend fun getAllSessions(): List { - return listOfNotNull(sessionDataFlow.value) - } - - override suspend fun getLatestSession(): SessionData? { - return sessionDataFlow.value - } - - override suspend fun removeSession(sessionId: String) { - if (sessionDataFlow.value?.userId == sessionId) { - sessionDataFlow.value = null - } - } -} diff --git a/libraries/session-storage/impl/build.gradle.kts b/libraries/session-storage/impl/build.gradle.kts index 76462ef035..3ceb4076cc 100644 --- a/libraries/session-storage/impl/build.gradle.kts +++ b/libraries/session-storage/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -15,10 +16,9 @@ android { namespace = "io.element.android.libraries.sessionstorage.impl" } -setupAnvil() +setupDependencyInjection() dependencies { - implementation(libs.dagger) implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.encryptedDb) @@ -29,18 +29,14 @@ dependencies { implementation(projects.libraries.di) implementation(libs.sqldelight.coroutines) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.coroutines.test) + testCommonDependencies(libs) testImplementation(libs.sqldelight.driver.jvm) - testImplementation(projects.tests.testutils) } sqldelight { databases { create("SessionDatabase") { - // https://cashapp.github.io/sqldelight/2.0.0/android_sqlite/migrations/ + // https://sqldelight.github.io/sqldelight/2.1.0/android_sqlite/migrations/ // To generate a .db file from your latest schema, run this task // ./gradlew generateDebugSessionDatabaseSchema // Test migration by running 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 d65fa5da90..d6197d868d 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 @@ -10,10 +10,11 @@ package io.element.android.libraries.sessionstorage.impl import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOneOrNull -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore @@ -22,18 +23,18 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber -import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DatabaseSessionStore @Inject constructor( +@Inject +class DatabaseSessionStore( private val database: SessionDatabase, private val dispatchers: CoroutineDispatchers, ) : SessionStore { private val sessionDataMutex = Mutex() - override fun isLoggedIn(): Flow { - return database.sessionDataQueries.selectFirst() + override fun loggedInStateFlow(): Flow { + return database.sessionDataQueries.selectLatest() .asFlow() .mapToOneOrNull(dispatchers.io) .map { @@ -48,9 +49,19 @@ class DatabaseSessionStore @Inject constructor( } } - override suspend fun storeData(sessionData: SessionData) { + override suspend fun addSession(sessionData: SessionData) { sessionDataMutex.withLock { - database.sessionDataQueries.insertSessionData(sessionData.toDbModel()) + val lastUsageIndex = getLastUsageIndex() + database.sessionDataQueries.insertSessionData( + sessionData + .copy( + // position value does not really matter, so just use lastUsageIndex + 1 to ensure that + // the value is always greater than value of any existing account + position = lastUsageIndex + 1, + lastUsageIndex = lastUsageIndex + 1, + ) + .toDbModel() + ) } } @@ -64,18 +75,71 @@ class DatabaseSessionStore @Inject constructor( Timber.e("User ${sessionData.userId} not found in session database") return } - // Copy new data from SDK, but keep login timestamp + // Copy new data from SDK, but keep application data database.sessionDataQueries.updateSession( sessionData.copy( loginTimestamp = result.loginTimestamp, + position = result.position, + lastUsageIndex = result.lastUsageIndex, + userDisplayName = result.userDisplayName, + userAvatarUrl = result.userAvatarUrl, ).toDbModel() ) } } + override suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?) { + sessionDataMutex.withLock { + val result = database.sessionDataQueries.selectByUserId(sessionId) + .executeAsOneOrNull() + ?.toApiModel() + if (result == null) { + Timber.e("User $sessionId not found in session database") + return + } + database.sessionDataQueries.updateSession( + result.copy( + userDisplayName = displayName, + userAvatarUrl = avatarUrl, + ).toDbModel() + ) + } + } + + override suspend fun setLatestSession(sessionId: String) { + val latestSession = getLatestSession() + if (latestSession?.userId == sessionId) { + // Already the latest session + return + } + val lastUsageIndex = latestSession?.lastUsageIndex ?: 0 + val result = database.sessionDataQueries.selectByUserId(sessionId) + .executeAsOneOrNull() + ?.toApiModel() + if (result == null) { + Timber.e("User $sessionId not found in session database") + return + } + sessionDataMutex.withLock { + // Update lastUsageIndex of the session + database.sessionDataQueries.updateSession( + result.copy( + lastUsageIndex = lastUsageIndex + 1, + ).toDbModel() + ) + } + } + + private fun getLastUsageIndex(): Long { + return database.sessionDataQueries.selectLatest() + .executeAsOneOrNull() + ?.lastUsageIndex + ?: -1L + } + override suspend fun getLatestSession(): SessionData? { return sessionDataMutex.withLock { - database.sessionDataQueries.selectFirst() + database.sessionDataQueries.selectLatest() .executeAsOneOrNull() ?.toApiModel() } diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt index 8dbbad2b71..3b694c0124 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt @@ -27,6 +27,10 @@ internal fun SessionData.toDbModel(): DbSessionData { passphrase = passphrase, sessionPath = sessionPath, cachePath = cachePath, + position = position, + lastUsageIndex = lastUsageIndex, + userDisplayName = userDisplayName, + userAvatarUrl = userAvatarUrl, ) } @@ -45,5 +49,9 @@ internal fun DbSessionData.toApiModel(): SessionData { passphrase = passphrase, sessionPath = sessionPath, cachePath = cachePath, + position = position, + lastUsageIndex = lastUsageIndex, + userDisplayName = userDisplayName, + userAvatarUrl = userAvatarUrl, ) } diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt index f9730f8095..00bdfaf587 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt @@ -8,17 +8,17 @@ package io.element.android.libraries.sessionstorage.impl.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.sessionstorage.impl.SessionDatabase import io.element.encrypteddb.SqlCipherDriverFactory import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider -@Module +@BindingContainer @ContributesTo(AppScope::class) object SessionStorageModule { @Provides 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 71fc9b6a98..78d596e5a6 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 @@ -7,10 +7,11 @@ package io.element.android.libraries.sessionstorage.impl.observer -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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 @@ -23,11 +24,11 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.CopyOnWriteArraySet -import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultSessionObserver @Inject constructor( +@Inject +class DefaultSessionObserver( private val sessionStore: SessionStore, @AppCoroutineScope private val coroutineScope: CoroutineScope, diff --git a/libraries/session-storage/impl/src/main/sqldelight/databases/10.db b/libraries/session-storage/impl/src/main/sqldelight/databases/10.db new file mode 100644 index 0000000000..fe31cc0fac Binary files /dev/null and b/libraries/session-storage/impl/src/main/sqldelight/databases/10.db differ diff --git a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq index 6e4c817475..53d07bfba3 100644 --- a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq +++ b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq @@ -27,15 +27,25 @@ CREATE TABLE SessionData ( -- added in version 6 sessionPath TEXT NOT NULL DEFAULT "", -- added in version 9 - cachePath TEXT NOT NULL DEFAULT "" + cachePath TEXT NOT NULL DEFAULT "", + -- added in version 10 + -- position, to be able to sort account by session creation date + position INTEGER NOT NULL DEFAULT 0, + -- index of the last usage session. Each time the current session change, the index of the current + -- session is incremented to the max value + 1 so it becomes the current session + lastUsageIndex INTEGER NOT NULL DEFAULT 0, + -- user display name + userDisplayName TEXT, + -- user avatar url + userAvatarUrl TEXT ); -selectFirst: -SELECT * FROM SessionData LIMIT 1; +selectLatest: +SELECT * FROM SessionData ORDER BY lastUsageIndex DESC LIMIT 1; selectAll: -SELECT * FROM SessionData; +SELECT * FROM SessionData ORDER BY lastUsageIndex DESC; selectByUserId: SELECT * FROM SessionData WHERE userId = ?; diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/9.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/9.sqm new file mode 100644 index 0000000000..51c1366525 --- /dev/null +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/9.sqm @@ -0,0 +1,9 @@ +-- Migrate DB from version 9 +-- Add position to be able to sort account by session creation date +-- Add lastUsageIndex so we can restore the last session and switch to another one +-- Add display name and avatar url of the user so that we can display a list of accounts. + +ALTER TABLE SessionData ADD COLUMN position INTEGER NOT NULL DEFAULT 0; +ALTER TABLE SessionData ADD COLUMN lastUsageIndex INTEGER NOT NULL DEFAULT 0; +ALTER TABLE SessionData ADD COLUMN userDisplayName TEXT; +ALTER TABLE SessionData ADD COLUMN userAvatarUrl TEXT; diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTest.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTest.kt index 0d74dde16a..7d264f42db 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTest.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTest.kt @@ -13,6 +13,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.session.SessionData import io.element.android.libraries.sessionstorage.api.LoggedInState +import io.element.android.libraries.sessionstorage.api.LoginType import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -44,22 +45,29 @@ class DatabaseSessionStoreTest { } @Test - fun `storeData persists the SessionData into the DB`() = runTest { - assertThat(database.sessionDataQueries.selectFirst().executeAsOneOrNull()).isNull() + fun `addSession persists the SessionData into the DB`() = runTest { + assertThat(database.sessionDataQueries.selectLatest().executeAsOneOrNull()).isNull() - databaseSessionStore.storeData(aSessionData.toApiModel()) + databaseSessionStore.addSession(aSessionData.toApiModel()) - assertThat(database.sessionDataQueries.selectFirst().executeAsOneOrNull()).isEqualTo(aSessionData) + assertThat(database.sessionDataQueries.selectLatest().executeAsOneOrNull()).isEqualTo(aSessionData) assertThat(database.sessionDataQueries.selectAll().executeAsList().size).isEqualTo(1) } @Test - fun `isLoggedIn emits true while there are sessions in the DB`() = runTest { - databaseSessionStore.isLoggedIn().test { + fun `loggedInStateFlow emits LoggedIn while there are sessions in the DB`() = runTest { + databaseSessionStore.loggedInStateFlow().test { assertThat(awaitItem()).isEqualTo(LoggedInState.NotLoggedIn) - database.sessionDataQueries.insertSessionData(aSessionData) + databaseSessionStore.addSession(aSessionData.toApiModel()) assertThat(awaitItem()).isEqualTo(LoggedInState.LoggedIn(sessionId = aSessionData.userId, isTokenValid = true)) - database.sessionDataQueries.removeSession(aSessionData.userId) + // Add a second session + databaseSessionStore.addSession(aSessionData.copy(userId = "otherUserId").toApiModel()) + assertThat(awaitItem()).isEqualTo(LoggedInState.LoggedIn(sessionId = "otherUserId", isTokenValid = true)) + // Remove the second session + databaseSessionStore.removeSession("otherUserId") + assertThat(awaitItem()).isEqualTo(LoggedInState.LoggedIn(sessionId = aSessionData.userId, isTokenValid = true)) + // Remove the first session + databaseSessionStore.removeSession(aSessionData.userId) assertThat(awaitItem()).isEqualTo(LoggedInState.NotLoggedIn) } } @@ -122,7 +130,83 @@ class DatabaseSessionStoreTest { } @Test - fun `update session update all fields except loginTimestamp`() = runTest { + fun `updateUserProfile does nothing if the session is not found`() = runTest { + databaseSessionStore.updateUserProfile(aSessionData.userId, "userDisplayName", "userAvatarUrl") + assertThat(database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOneOrNull()).isNull() + } + + @Test + fun `updateUserProfile update the data`() = runTest { + database.sessionDataQueries.insertSessionData(aSessionData) + databaseSessionStore.updateUserProfile(aSessionData.userId, "userDisplayName", "userAvatarUrl") + val updatedSession = database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOne() + assertThat(updatedSession.userDisplayName).isEqualTo("userDisplayName") + assertThat(updatedSession.userAvatarUrl).isEqualTo("userAvatarUrl") + } + + @Test + fun `setLatestSession is no op when the session is already the latest session`() = runTest { + database.sessionDataQueries.insertSessionData(aSessionData) + val session = database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOne() + assertThat(session.lastUsageIndex).isEqualTo(0) + assertThat(session.position).isEqualTo(0) + databaseSessionStore.setLatestSession(aSessionData.userId) + assertThat(database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOne().lastUsageIndex).isEqualTo(0) + } + + @Test + fun `setLatestSession is no op when the session is not found`() = runTest { + databaseSessionStore.setLatestSession(aSessionData.userId) + } + + @Test + fun `multi session test`() = runTest { + databaseSessionStore.addSession(aSessionData.toApiModel()) + val session = databaseSessionStore.getSession(aSessionData.userId)!! + assertThat(session.lastUsageIndex).isEqualTo(0) + assertThat(session.position).isEqualTo(0) + val secondSessionData = aSessionData.copy( + userId = "otherUserId", + position = 1, + lastUsageIndex = 1, + ) + databaseSessionStore.addSession(secondSessionData.toApiModel()) + val secondSession = database.sessionDataQueries.selectByUserId(secondSessionData.userId).executeAsOne() + assertThat(secondSession.lastUsageIndex).isEqualTo(1) + assertThat(secondSession.position).isEqualTo(1) + // Set the first session as the latest + databaseSessionStore.setLatestSession(aSessionData.userId) + val firstSession = database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOne() + assertThat(firstSession.lastUsageIndex).isEqualTo(2) + assertThat(firstSession.position).isEqualTo(0) + // Check that the second session has not been altered + val secondSession2 = database.sessionDataQueries.selectByUserId(secondSessionData.userId).executeAsOne() + assertThat(secondSession2.lastUsageIndex).isEqualTo(1) + assertThat(secondSession2.position).isEqualTo(1) + } + + @Test + fun `test sessionsFlow()`() = runTest { + databaseSessionStore.sessionsFlow().test { + assertThat(awaitItem()).isEmpty() + databaseSessionStore.addSession(aSessionData.toApiModel()) + assertThat(awaitItem().size).isEqualTo(1) + val secondSessionData = aSessionData.copy( + userId = "otherUserId", + position = 1, + lastUsageIndex = 1, + ) + databaseSessionStore.addSession(secondSessionData.toApiModel()) + assertThat(awaitItem().size).isEqualTo(2) + databaseSessionStore.removeSession(aSessionData.userId) + assertThat(awaitItem().size).isEqualTo(1) + databaseSessionStore.removeSession(secondSessionData.userId) + assertThat(awaitItem()).isEmpty() + } + } + + @Test + fun `update session update all fields except info used by the application`() = runTest { val firstSessionData = SessionData( userId = "userId", deviceId = "deviceId", @@ -137,6 +221,10 @@ class DatabaseSessionStoreTest { passphrase = "aPassphrase", sessionPath = "sessionPath", cachePath = "cachePath", + position = 0, + lastUsageIndex = 0, + userDisplayName = "userDisplayName", + userAvatarUrl = "userAvatarUrl", ) val secondSessionData = SessionData( userId = "userId", @@ -150,8 +238,12 @@ class DatabaseSessionStoreTest { isTokenValid = 1, loginType = null, passphrase = "aPassphraseAltered", - sessionPath = "sessionPath", - cachePath = "cachePath", + sessionPath = "sessionPathAltered", + cachePath = "cachePathAltered", + position = 1, + lastUsageIndex = 1, + userDisplayName = "userDisplayNameAltered", + userAvatarUrl = "userAvatarUrlAltered", ) assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId) assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp) @@ -172,6 +264,11 @@ class DatabaseSessionStoreTest { assertThat(alteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp) assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData) assertThat(alteredSession.passphrase).isEqualTo(secondSessionData.passphrase) + // Check that application data have not been altered + assertThat(alteredSession.position).isEqualTo(firstSessionData.position) + assertThat(alteredSession.lastUsageIndex).isEqualTo(firstSessionData.lastUsageIndex) + assertThat(alteredSession.userDisplayName).isEqualTo(firstSessionData.userDisplayName) + assertThat(alteredSession.userAvatarUrl).isEqualTo(firstSessionData.userAvatarUrl) } @Test @@ -186,10 +283,14 @@ class DatabaseSessionStoreTest { loginTimestamp = 1, oidcData = "aOidcData", isTokenValid = 1, - loginType = null, + loginType = LoginType.PASSWORD.name, passphrase = "aPassphrase", sessionPath = "sessionPath", cachePath = "cachePath", + position = 0, + lastUsageIndex = 0, + userDisplayName = "userDisplayName", + userAvatarUrl = "userAvatarUrl", ) val secondSessionData = SessionData( userId = "userIdUnknown", @@ -201,10 +302,14 @@ class DatabaseSessionStoreTest { loginTimestamp = 2, oidcData = "aOidcDataAltered", isTokenValid = 1, - loginType = null, + loginType = LoginType.PASSWORD.name, passphrase = "aPassphraseAltered", - sessionPath = "sessionPath", - cachePath = "cachePath", + sessionPath = "sessionPathAltered", + cachePath = "cachePathAltered", + position = 1, + lastUsageIndex = 1, + userDisplayName = "userDisplayNameAltered", + userAvatarUrl = "userAvatarUrlAltered", ) assertThat(firstSessionData.userId).isNotEqualTo(secondSessionData.userId) @@ -214,14 +319,6 @@ class DatabaseSessionStoreTest { // Get the session and check that it has not been altered val notAlteredSession = databaseSessionStore.getSession(firstSessionData.userId)!!.toDbModel() - assertThat(notAlteredSession.userId).isEqualTo(firstSessionData.userId) - assertThat(notAlteredSession.deviceId).isEqualTo(firstSessionData.deviceId) - assertThat(notAlteredSession.accessToken).isEqualTo(firstSessionData.accessToken) - assertThat(notAlteredSession.refreshToken).isEqualTo(firstSessionData.refreshToken) - assertThat(notAlteredSession.homeserverUrl).isEqualTo(firstSessionData.homeserverUrl) - assertThat(notAlteredSession.slidingSyncProxy).isEqualTo(firstSessionData.slidingSyncProxy) - assertThat(notAlteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp) - assertThat(notAlteredSession.oidcData).isEqualTo(firstSessionData.oidcData) - assertThat(notAlteredSession.passphrase).isEqualTo(firstSessionData.passphrase) + assertThat(notAlteredSession).isEqualTo(firstSessionData) } } diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt index 3251dfc5d9..e8713dac1a 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt @@ -24,4 +24,8 @@ internal fun aSessionData() = SessionData( passphrase = null, sessionPath = "sessionPath", cachePath = "cachePath", + position = 0, + lastUsageIndex = 0, + userDisplayName = null, + userAvatarUrl = null, ) diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserverTest.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserverTest.kt index 35d7fea042..8b0184fdc7 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserverTest.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserverTest.kt @@ -51,7 +51,7 @@ import org.junit.Test runCurrent() val listener = TestSessionListener() sut.addListener(listener) - databaseSessionStore.storeData(sessionData.toApiModel()) + databaseSessionStore.addSession(sessionData.toApiModel()) listener.assertEvents(TestSessionListener.Event.Created(sessionData.userId)) sut.removeListener(listener) coroutineContext.cancelChildren() @@ -64,7 +64,7 @@ import org.junit.Test runCurrent() val listener = TestSessionListener() sut.addListener(listener) - databaseSessionStore.storeData(sessionData.toApiModel()) + databaseSessionStore.addSession(sessionData.toApiModel()) listener.assertEvents(TestSessionListener.Event.Created(sessionData.userId)) databaseSessionStore.removeSession(sessionData.userId) listener.assertEvents( diff --git a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemorySessionStore.kt b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemorySessionStore.kt new file mode 100644 index 0000000000..c8f3078e7a --- /dev/null +++ b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemorySessionStore.kt @@ -0,0 +1,83 @@ +/* + * 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.sessionstorage.test + +import io.element.android.libraries.sessionstorage.api.LoggedInState +import io.element.android.libraries.sessionstorage.api.SessionData +import io.element.android.libraries.sessionstorage.api.SessionStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map + +class InMemorySessionStore( + initialList: List = emptyList(), + private val updateUserProfileResult: (String, String?, String?) -> Unit = { _, _, _ -> error("Not implemented") }, + private val setLatestSessionResult: (String) -> Unit = { error("Not implemented") }, +) : SessionStore { + private val sessionDataListFlow = MutableStateFlow(initialList) + + override fun loggedInStateFlow(): Flow { + return sessionDataListFlow.map { + if (it.isEmpty()) { + LoggedInState.NotLoggedIn + } else { + it.first().let { sessionData -> + LoggedInState.LoggedIn( + sessionId = sessionData.userId, + isTokenValid = sessionData.isTokenValid, + ) + } + } + } + } + + override fun sessionsFlow(): Flow> = sessionDataListFlow.asStateFlow() + + override suspend fun addSession(sessionData: SessionData) { + val currentList = sessionDataListFlow.value.toMutableList() + currentList.removeAll { it.userId == sessionData.userId } + currentList.add(sessionData) + sessionDataListFlow.value = currentList + } + + override suspend fun updateData(sessionData: SessionData) { + val currentList = sessionDataListFlow.value.toMutableList() + val index = currentList.indexOfFirst { it.userId == sessionData.userId } + if (index != -1) { + currentList[index] = sessionData + sessionDataListFlow.value = currentList + } + } + + override suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?) { + updateUserProfileResult(sessionId, displayName, avatarUrl) + } + + override suspend fun getSession(sessionId: String): SessionData? { + return sessionDataListFlow.value.firstOrNull { it.userId == sessionId } + } + + override suspend fun getAllSessions(): List { + return sessionDataListFlow.value + } + + override suspend fun getLatestSession(): SessionData? { + return sessionDataListFlow.value.firstOrNull() + } + + override suspend fun setLatestSession(sessionId: String) { + setLatestSessionResult(sessionId) + } + + override suspend fun removeSession(sessionId: String) { + val currentList = sessionDataListFlow.value.toMutableList() + currentList.removeAll { it.userId == sessionId } + sessionDataListFlow.value = currentList + } +} diff --git a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt index afff40b6e1..61b5370813 100644 --- a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt +++ b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt @@ -18,7 +18,11 @@ fun aSessionData( cachePath: String = "/a/path/to/a/cache", accessToken: String = "anAccessToken", refreshToken: String? = "aRefreshToken", - ): SessionData { + position: Long = 0, + lastUsageIndex: Long = 0, + userDisplayName: String? = null, + userAvatarUrl: String? = null, +): SessionData { return SessionData( userId = sessionId, deviceId = deviceId, @@ -33,5 +37,9 @@ fun aSessionData( passphrase = null, sessionPath = sessionPath, cachePath = cachePath, + position = position, + lastUsageIndex = lastUsageIndex, + userDisplayName = userDisplayName, + userAvatarUrl = userAvatarUrl, ) } diff --git a/libraries/textcomposer/impl/build.gradle.kts b/libraries/textcomposer/impl/build.gradle.kts index 0c8af9a636..6f735bec23 100644 --- a/libraries/textcomposer/impl/build.gradle.kts +++ b/libraries/textcomposer/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -19,7 +20,7 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.architecture) @@ -42,14 +43,6 @@ dependencies { debugApi(libs.matrix.richtexteditor.compose) } - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } 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 index f4b5f96bf4..4c74a6edd5 100644 --- 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 @@ -7,13 +7,13 @@ package io.element.android.libraries.textcomposer.mentions -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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" // 💬 @@ -28,7 +28,8 @@ interface MentionSpanFormatter { * based on its MentionType and context. */ @ContributesBinding(RoomScope::class) -class DefaultMentionSpanFormatter @Inject constructor( +@Inject +class DefaultMentionSpanFormatter( private val roomMemberProfilesCache: RoomMemberProfilesCache, private val roomNamesCache: RoomNamesCache, ) : MentionSpanFormatter { 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 a233897254..edaf417db7 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,19 +7,19 @@ package io.element.android.libraries.textcomposer.mentions +import dev.zacsweers.metro.Inject 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 private const val EVERYONE_MENTION_TEXT = "@room" /** * Provider for [MentionSpan]s. */ -open class MentionSpanProvider @Inject constructor( +@Inject open class MentionSpanProvider( private val permalinkParser: PermalinkParser, private val mentionSpanFormatter: MentionSpanFormatter, private val mentionSpanTheme: MentionSpanTheme, 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 4edbb8bf38..e23fc8e83a 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 @@ -30,6 +30,8 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.text.buildSpannedString +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -38,7 +40,6 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.messageFromMeBackground import io.element.android.libraries.designsystem.theme.messageFromOtherBackground 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 @@ -46,7 +47,6 @@ 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 kotlinx.collections.immutable.persistentListOf -import javax.inject.Inject /** * Theme used for mention spans. 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 index 94d74a72e3..9a9714b351 100644 --- 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 @@ -12,12 +12,12 @@ 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 dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 @@ -27,7 +27,8 @@ interface MentionSpanUpdater { } @ContributesBinding(RoomScope::class) -class DefaultMentionSpanUpdater @Inject constructor( +@Inject +class DefaultMentionSpanUpdater( private val formatter: MentionSpanFormatter, private val theme: MentionSpanTheme, private val roomMemberProfilesCache: RoomMemberProfilesCache, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt index bb9670de39..ab5d3bb4e1 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.textcomposer.model import androidx.compose.runtime.Immutable import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails @@ -49,7 +50,7 @@ sealed interface MessageComposerMode { get() = this is Reply && replyToDetails is InReplyToDetails.Ready && replyToDetails.eventContent is MessageContent && - (replyToDetails.eventContent as MessageContent).threadInfo.threadRootId != null + (replyToDetails.eventContent as MessageContent).threadInfo is EventThreadInfo.ThreadResponse } fun MessageComposerMode.showCaptionCompatibilityWarning(): Boolean { diff --git a/libraries/textcomposer/impl/src/main/res/values-bg/translations.xml b/libraries/textcomposer/impl/src/main/res/values-bg/translations.xml index 3079cc1054..b0dfde1c2e 100644 --- a/libraries/textcomposer/impl/src/main/res/values-bg/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-bg/translations.xml @@ -1,6 +1,8 @@ "Прикачване на файл" + "Отказ и затваряне на форматирането на текст" + "Превключване на кодов блок" "Съобщение…" "Създаване на връзка" "Редактиране на връзката" @@ -8,8 +10,15 @@ "Прилагане на курсив формат" "Прилагане на зачеркнат формат" "Прилагане на формат за подчертаване" + "Превключване на режим на цял екран" + "Отстъп навътре" "Прилагане на формат на вграден код" + "Задаване на връзка" + "Превключване на номериран списък" + "Отваряне на опциите за съставяне" "Превключване на цитат" "Премахване на връзката" + "Отстъп навън" "Връзка" + "Задръжте, за записване" diff --git a/libraries/textcomposer/impl/src/main/res/values-eu/translations.xml b/libraries/textcomposer/impl/src/main/res/values-eu/translations.xml index 8e8571e527..597f2fce98 100644 --- a/libraries/textcomposer/impl/src/main/res/values-eu/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-eu/translations.xml @@ -2,7 +2,8 @@ "Gehitu eranskina" "Buleten zerrenda bai/ez" - "Itxi formatu aukerak" + "Baztertu eta itxi formatu aukerak" + "Kode-blokea bai/ez" "Gehitu testua" "Zifratutako mezua…" "Mezua…" diff --git a/libraries/textcomposer/impl/src/main/res/values-ko/translations.xml b/libraries/textcomposer/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..40f030f3e7 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,33 @@ + + + "첨부파일 추가" + "글머리 기호 목록 전환" + "텍스트 서식 취소 및 닫기" + "코드 블록 전환" + "캡션을 추가하세요" + "암호화된 메세지…" + "메시지…" + "비암호화된 메시지…" + "링크 생성" + "링크 수정" + "%1$s, 상태: %2$s" + "굵음 적용" + "기울임 적용" + "비활성화됨" + "끄기" + "켜기" + "취소선 적용" + "밑줄 적용" + "전체화면 모드 전환" + "들여쓰기" + "인라인 코드 형식 적용" + "링크 설정" + "숫자 목록 전환" + "작성 옵션 열기" + "인용 전환" + "링크 제거" + "들여쓰기 취소" + "링크" + "캡션은 오래된 앱을 사용하는 사용자에게 표시되지 않을 수 있습니다." + "녹음하려면 길게 누르세요." + 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 5f43f66370..76b9caee0b 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 @@ -1,9 +1,9 @@ "Adicionar anexo" - "Alternar lista de marcadores" - "Fechar opções de formatação" - "Alternar bloco de código" + "Habilitar lista de objetivos" + "Cancelar e fechar opções de formatação" + "Habilitar bloco de código" "Adicionar uma legenda" "Mensagem criptografada…" "Mensagem…" @@ -11,23 +11,23 @@ "Criar um link" "Editar link" "%1$s, estado: %2$s" - "Aplicar negrito" - "Aplicar formato itálico" + "Aplicar formato em negrito" + "Aplicar itálico" "desativado" "desligado" "ligado" - "Aplicar formato tachado" + "Aplicar risco" "Aplicar sublinhado" - "Alternar o modo de tela cheia" + "Habilitar o modo de tela cheia" "Identar" - "Aplicar formato de código embutido" + "Aplicar código na mesma linha" "Definir link" - "Alternar lista numerada" + "Habilitar lista numerada" "Abrir opções de composição" - "Alternar citação" + "Habilitar citação" "Remover link" "Desidentar" "Link" - "As legendas podem não ser visíveis para pessoas que usam aplicativos mais antigos." + "As legendas podem não ser visíveis para pessoas que usam apps mais antigos." "Segure para gravar" diff --git a/libraries/textcomposer/impl/src/main/res/values-ro/translations.xml b/libraries/textcomposer/impl/src/main/res/values-ro/translations.xml index 62e3b79ea2..cdfaabd496 100644 --- a/libraries/textcomposer/impl/src/main/res/values-ro/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-ro/translations.xml @@ -4,11 +4,18 @@ "Comutați lista cu puncte" "Închideți opțiunile de formatare" "Comutați blocul de cod" + "Adăugați o descriere" + "Mesaj criptat…" "Mesaj…" + "Mesaj necriptat…" "Creați un link" "Editați link-ul" + "%1$s, stare: %2$s" "Aplicați formatul aldin" "Aplicați formatul italic" + "dezactivat" + "dezactivat" + "activat" "Aplicați formatul barat" "Aplică formatul de subliniere" "Comutați modul ecran complet" @@ -21,5 +28,6 @@ "Ștergeți linkul" "Dez-identare" "Link" + "Este posibil ca descrierile să nu fie vizibile pentru persoanele care folosesc aplicații mai vechi." "Țineți apăsat pentru a înregistra" 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 3bb14884ac..589a1f940c 100644 --- a/libraries/textcomposer/impl/src/main/res/values-ru/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-ru/translations.xml @@ -2,7 +2,7 @@ "Прикрепить файл" "Переключить список маркеров" - "Закрыть параметры форматирования" + "Отменить и закрыть параметры форматирования" "Переключить блок кода" "Необязательный заголовок…" "Зашифрованное сообщение…" @@ -10,8 +10,12 @@ "Незашифрованное сообщение…" "Создать ссылку" "Редактировать ссылку" + "%1$s, состояние: %2$s" "Применить жирный шрифт" "Применить курсивный формат" + "отключено" + "ОТКЛ." + "ВКЛ" "Применить формат зачеркивания" "Применить формат подчеркивания" "Переключение полноэкранного режима" diff --git a/libraries/textcomposer/impl/src/main/res/values-tr/translations.xml b/libraries/textcomposer/impl/src/main/res/values-tr/translations.xml index 4d1fadb3cd..e617fdfb09 100644 --- a/libraries/textcomposer/impl/src/main/res/values-tr/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-tr/translations.xml @@ -2,10 +2,12 @@ "Ek ekle" "Madde işaretli listeyi aç/kapat" - "Biçimlendirme seçeneklerini kapat" + "İptal et ve biçimlendirme seçeneklerini kapat" "Kod Bloğunu Aç/Kapat" "Açıklama ekle" + "Şifrelenmiş mesaj…" "Mesaj…" + "Şifrelenmemiş mesaj…" "Bir bağlantı oluştur" "Bağlantıyı Düzenle" "Kalın biçimi uygula" 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 cef6f065c2..87e83ad6d6 100644 --- a/libraries/textcomposer/impl/src/main/res/values-uk/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-uk/translations.xml @@ -13,7 +13,9 @@ "%1$s, стан: %2$s" "Жирний формат" "Курсивний формат" + "вимкнено" "вимкнено" + "увімкнено" "Застосувати формат закреслення" "Застосувати формат підкреслення" "Перемкнути повноекранний режим" diff --git a/libraries/textcomposer/impl/src/main/res/values-uz/translations.xml b/libraries/textcomposer/impl/src/main/res/values-uz/translations.xml index bc6cd60cb1..9b28fd8ef6 100644 --- a/libraries/textcomposer/impl/src/main/res/values-uz/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-uz/translations.xml @@ -4,6 +4,7 @@ "Belgilar roʻyxatini almashtirish" "Formatlash parametrlarini yoping" "Kod blokini almashtirish" + "Taglavha kiritish" "Xabar…" "Havola yarating" "Havolani tahrirlash" 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 b8c6ba98bd..8db2b9c767 100644 --- a/libraries/textcomposer/impl/src/main/res/values-zh/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-zh/translations.xml @@ -2,7 +2,7 @@ "添加附件" "切换符号列表" - "关闭格式化选项" + "取消并关闭文本格式" "切换代码块" "可选的标题……" "加密信息…" diff --git a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/NotificationTroubleShootEntryPoint.kt b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/NotificationTroubleShootEntryPoint.kt index bc3b019ba3..bf0c6bb883 100644 --- a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/NotificationTroubleShootEntryPoint.kt +++ b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/NotificationTroubleShootEntryPoint.kt @@ -22,5 +22,6 @@ interface NotificationTroubleShootEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onDone() + fun openIgnoredUsers() } } 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 index 088fb387da..0eab9b8e5a 100644 --- 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 @@ -13,7 +13,6 @@ 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 @@ -25,6 +24,6 @@ interface PushHistoryEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onDone() - fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId) + fun navigateTo(roomId: RoomId, eventId: EventId) } } diff --git a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootNavigator.kt b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootNavigator.kt new file mode 100644 index 0000000000..0cce358072 --- /dev/null +++ b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootNavigator.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.troubleshoot.api.test + +interface NotificationTroubleshootNavigator { + fun openIgnoredUsers() +} diff --git a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTest.kt b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTest.kt index d20370729e..f729cd3300 100644 --- a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTest.kt +++ b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTest.kt @@ -16,7 +16,10 @@ interface NotificationTroubleshootTest { fun isRelevant(data: TestFilterData): Boolean = true suspend fun run(coroutineScope: CoroutineScope) suspend fun reset() - suspend fun quickFix(coroutineScope: CoroutineScope) { + suspend fun quickFix( + coroutineScope: CoroutineScope, + navigator: NotificationTroubleshootNavigator, + ) { error("Quick fix not implemented, you need to override this method in your test") } } diff --git a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTestDelegate.kt b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTestDelegate.kt index 561ec60267..830ba74818 100644 --- a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTestDelegate.kt +++ b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTestDelegate.kt @@ -60,7 +60,7 @@ class NotificationTroubleshootTestDelegate( if (isSuccess) { NotificationTroubleshootTestState.Status.Success } else { - NotificationTroubleshootTestState.Status.Failure(hasQuickFix) + NotificationTroubleshootTestState.Status.Failure(hasQuickFix = hasQuickFix) } ) } diff --git a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTestState.kt b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTestState.kt index d845332df3..4069d78cb3 100644 --- a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTestState.kt +++ b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTestState.kt @@ -17,6 +17,10 @@ data class NotificationTroubleshootTestState( data object InProgress : Status data object WaitingForUser : Status data object Success : Status - data class Failure(val hasQuickFix: Boolean) : Status + data class Failure( + val hasQuickFix: Boolean = false, + val isCritical: Boolean = true, + val quickFixButtonString: String? = null, + ) : Status } } diff --git a/libraries/troubleshoot/impl/build.gradle.kts b/libraries/troubleshoot/impl/build.gradle.kts index f88ccc8f85..bcc7c4357b 100644 --- a/libraries/troubleshoot/impl/build.gradle.kts +++ b/libraries/troubleshoot/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -21,10 +22,9 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { - implementation(libs.dagger) implementation(projects.libraries.architecture) implementation(projects.libraries.designsystem) implementation(projects.libraries.di) @@ -34,16 +34,8 @@ dependencies { api(projects.libraries.push.api) implementation(projects.services.analytics.api) - testImplementation(libs.test.junit) - testImplementation(libs.test.robolectric) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.coroutines.test) + testCommonDependencies(libs, true) 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/DefaultNotificationTroubleShootEntryPoint.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/DefaultNotificationTroubleShootEntryPoint.kt index aa7e55b07a..b9d9c91814 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/DefaultNotificationTroubleShootEntryPoint.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/DefaultNotificationTroubleShootEntryPoint.kt @@ -10,14 +10,15 @@ package io.element.android.libraries.troubleshoot.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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultNotificationTroubleShootEntryPoint @Inject constructor() : NotificationTroubleShootEntryPoint { +@Inject +class DefaultNotificationTroubleShootEntryPoint : NotificationTroubleShootEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NotificationTroubleShootEntryPoint.NodeBuilder { val plugins = ArrayList() diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsNode.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsNode.kt index 72c999835a..508010a3d6 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsNode.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsNode.kt @@ -13,27 +13,40 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.ContributesNode import io.element.android.libraries.di.SessionScope import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator import io.element.android.services.analytics.api.ScreenTracker @ContributesNode(SessionScope::class) -class TroubleshootNotificationsNode @AssistedInject constructor( +@AssistedInject +class TroubleshootNotificationsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: TroubleshootNotificationsPresenter, private val screenTracker: ScreenTracker, -) : Node(buildContext, plugins = plugins) { + factory: TroubleshootNotificationsPresenter.Factory, +) : Node(buildContext, plugins = plugins), + NotificationTroubleshootNavigator { + private val presenter = factory.create( + navigator = this, + ) + private fun onDone() { plugins().forEach { it.onDone() } } + override fun openIgnoredUsers() { + plugins().forEach { + it.openIgnoredUsers() + } + } + @Composable override fun View(modifier: Modifier) { screenTracker.TrackScreen(MobileScreen.ScreenName.NotificationTroubleshoot) diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenter.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenter.kt index 9b6a2ce5fc..07840b023c 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenter.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenter.kt @@ -12,13 +12,23 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator import kotlinx.coroutines.launch -import javax.inject.Inject -class TroubleshootNotificationsPresenter @Inject constructor( +@AssistedInject +class TroubleshootNotificationsPresenter( + @Assisted private val navigator: NotificationTroubleshootNavigator, private val troubleshootTestSuite: TroubleshootTestSuite, ) : Presenter { + @AssistedFactory + fun interface Factory { + fun create(navigator: NotificationTroubleshootNavigator): TroubleshootNotificationsPresenter + } + @Composable override fun present(): TroubleshootNotificationsState { val coroutineScope = rememberCoroutineScope() @@ -33,7 +43,11 @@ class TroubleshootNotificationsPresenter @Inject constructor( troubleshootTestSuite.runTestSuite(this) } is TroubleshootNotificationsEvents.QuickFix -> coroutineScope.launch { - troubleshootTestSuite.quickFix(event.testIndex, this) + troubleshootTestSuite.quickFix( + testIndex = event.testIndex, + coroutineScope = this, + navigator = navigator, + ) } TroubleshootNotificationsEvents.RetryFailedTests -> coroutineScope.launch { troubleshootTestSuite.retryFailedTest(this) diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsStateProvider.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsStateProvider.kt index 4ea9355a8c..8c2708ed9d 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsStateProvider.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsStateProvider.kt @@ -31,8 +31,12 @@ open class TroubleshootNotificationsStateProvider : PreviewParameterProvider Unit, ) { - if ((testState.status as? Status.Idle)?.visible == false) return + val status = testState.status + if ((status as? Status.Idle)?.visible == false) return ListItem( headlineContent = { Text(text = testState.name) }, supportingContent = { Text(text = testState.description) }, - trailingContent = when (testState.status) { + trailingContent = when (status) { is Status.Idle -> null Status.InProgress -> ListItemContent.Custom { CircularProgressIndicator( @@ -98,20 +99,19 @@ private fun ColumnScope.TroubleshootTestView( Icon( contentDescription = null, modifier = Modifier.size(24.dp), - imageVector = CompoundIcons.ErrorSolid(), - tint = ElementTheme.colors.textCriticalPrimary + imageVector = if (status.isCritical) CompoundIcons.ErrorSolid() else CompoundIcons.Warning(), + tint = ElementTheme.colors.iconCriticalPrimary, ) } } ) - if ((testState.status as? Status.Failure)?.hasQuickFix == true) { + if (status is Status.Failure && status.hasQuickFix) { ListItem( - headlineContent = { - }, + headlineContent = { }, trailingContent = ListItemContent.Custom { Button( - text = stringResource(id = R.string.troubleshoot_notifications_screen_quick_fix_action), - onClick = onQuickFixClick + text = status.quickFixButtonString ?: stringResource(id = R.string.troubleshoot_notifications_screen_quick_fix_action), + onClick = onQuickFixClick, ) } ) diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootTestSuite.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootTestSuite.kt index bd4e899217..c26510fc92 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootTestSuite.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootTestSuite.kt @@ -7,9 +7,11 @@ package io.element.android.libraries.troubleshoot.impl +import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.NotificationTroubleshoot import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.push.api.GetCurrentPushProvider +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.TestFilterData @@ -20,9 +22,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import javax.inject.Inject -class TroubleshootTestSuite @Inject constructor( +@Inject +class TroubleshootTestSuite( private val notificationTroubleshootTests: Set<@JvmSuppressWildcards NotificationTroubleshootTest>, private val getCurrentPushProvider: GetCurrentPushProvider, private val analyticsService: AnalyticsService, @@ -89,8 +91,12 @@ class TroubleshootTestSuite @Inject constructor( ) } - suspend fun quickFix(testIndex: Int, coroutineScope: CoroutineScope) { - tests[testIndex].quickFix(coroutineScope) + suspend fun quickFix( + testIndex: Int, + coroutineScope: CoroutineScope, + navigator: NotificationTroubleshootNavigator, + ) { + tests[testIndex].quickFix(coroutineScope, navigator) } } @@ -103,7 +109,7 @@ fun List.computeMainState(): AsyncAction { if (any { it.status is NotificationTroubleshootTestState.Status.WaitingForUser }) { AsyncAction.ConfirmingNoParams - } else if (any { it.status is NotificationTroubleshootTestState.Status.Failure }) { + } else if (any { it.status.let { status -> status is NotificationTroubleshootTestState.Status.Failure && status.isCritical } }) { AsyncAction.Failure(Exception("Some tests failed")) } else { AsyncAction.Success(Unit) 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 index dd8f663414..9a33848cae 100644 --- 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 @@ -10,14 +10,15 @@ 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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject 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 { +@Inject +class DefaultPushHistoryEntryPoint : PushHistoryEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): PushHistoryEntryPoint.NodeBuilder { val plugins = ArrayList() 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 index c18a480899..893be607a0 100644 --- 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 @@ -7,8 +7,13 @@ package io.element.android.libraries.troubleshoot.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 + sealed interface PushHistoryEvents { data class SetShowOnlyErrors(val showOnlyErrors: Boolean) : PushHistoryEvents data class Reset(val requiresConfirmation: Boolean) : PushHistoryEvents + data class NavigateTo(val sessionId: SessionId, val roomId: RoomId, val eventId: EventId) : 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 index 347a1c2700..69070298ec 100644 --- 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 @@ -13,36 +13,38 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode +import io.element.android.annotations.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( +@AssistedInject +class PushHistoryNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: PushHistoryPresenter, + presenterFactory: PushHistoryPresenter.Factory, private val screenTracker: ScreenTracker, -) : Node(buildContext, plugins = plugins) { +) : Node(buildContext, plugins = plugins), PushHistoryNavigator { private fun onDone() { plugins().forEach { it.onDone() } } - private fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId) { + override fun navigateTo(roomId: RoomId, eventId: EventId) { plugins().forEach { - it.onItemClick(sessionId, roomId, eventId) + it.navigateTo(roomId, eventId) } } + private val presenter = presenterFactory.create(this) + @Composable override fun View(modifier: Modifier) { screenTracker.TrackScreen(MobileScreen.ScreenName.NotificationTroubleshoot) @@ -50,7 +52,6 @@ class PushHistoryNode @AssistedInject constructor( 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 index ec6f2f0abb..b98fcee970 100644 --- 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 @@ -14,17 +14,36 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter +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.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( +fun interface PushHistoryNavigator { + fun navigateTo(roomId: RoomId, eventId: EventId) +} + +@AssistedInject +class PushHistoryPresenter( + @Assisted private val pushHistoryNavigator: PushHistoryNavigator, private val pushService: PushService, + matrixClient: MatrixClient, ) : Presenter { + @AssistedFactory + fun interface Factory { + fun create(pushHistoryNavigator: PushHistoryNavigator): PushHistoryPresenter + } + + private val sessionId = matrixClient.sessionId + @Composable override fun present(): PushHistoryState { val coroutineScope = rememberCoroutineScope() @@ -40,6 +59,7 @@ class PushHistoryPresenter @Inject constructor( } }.collectAsState(emptyList()) var resetAction: AsyncAction by remember { mutableStateOf(AsyncAction.Uninitialized) } + var showNotSameAccountError by remember { mutableStateOf(false) } fun handleEvents(event: PushHistoryEvents) { when (event) { @@ -59,6 +79,14 @@ class PushHistoryPresenter @Inject constructor( } PushHistoryEvents.ClearDialog -> { resetAction = AsyncAction.Uninitialized + showNotSameAccountError = false + } + is PushHistoryEvents.NavigateTo -> { + if (event.sessionId != sessionId) { + showNotSameAccountError = true + } else { + pushHistoryNavigator.navigateTo(event.roomId, event.eventId) + } } } } @@ -68,6 +96,7 @@ class PushHistoryPresenter @Inject constructor( pushHistoryItems = pushHistory.toImmutableList(), showOnlyErrors = showOnlyErrors, resetAction = resetAction, + showNotSameAccountError = showNotSameAccountError, 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 index fda9c6e479..b4b6d7f75e 100644 --- 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 @@ -16,5 +16,6 @@ data class PushHistoryState( val pushHistoryItems: ImmutableList, val showOnlyErrors: Boolean, val resetAction: AsyncAction, + val showNotSameAccountError: Boolean, 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 index da37700a93..11d9c509cc 100644 --- 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 @@ -40,6 +40,9 @@ open class PushHistoryStateProvider : PreviewParameterProvider aPushHistoryState( resetAction = AsyncAction.ConfirmingNoParams, ), + aPushHistoryState( + showNotSameAccountError = true, + ), ) } @@ -48,12 +51,14 @@ fun aPushHistoryState( pushHistoryItems: List = emptyList(), showOnlyErrors: Boolean = false, resetAction: AsyncAction = AsyncAction.Uninitialized, + showNotSameAccountError: Boolean = false, eventSink: (PushHistoryEvents) -> Unit = {}, ) = PushHistoryState( pushCounter = pushCounter, pushHistoryItems = pushHistoryItems.toImmutableList(), showOnlyErrors = showOnlyErrors, resetAction = resetAction, + showNotSameAccountError = showNotSameAccountError, eventSink = eventSink, ) diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt index 2cd2e6dc20..3193716d34 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt @@ -37,6 +37,7 @@ 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.dialogs.ErrorDialog 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 @@ -48,9 +49,6 @@ 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 @@ -60,7 +58,6 @@ import io.element.android.libraries.ui.strings.CommonStrings fun PushHistoryView( state: PushHistoryState, onBackClick: () -> Unit, - onItemClick: (SessionId, RoomId, EventId) -> Unit, modifier: Modifier = Modifier, ) { var showMenu by remember { mutableStateOf(false) } @@ -123,7 +120,6 @@ fun PushHistoryView( .padding(padding) .consumeWindowInsets(padding), state = state, - onItemClick = onItemClick, ) } @@ -142,12 +138,18 @@ fun PushHistoryView( }, onErrorDismiss = {}, ) + + if (state.showNotSameAccountError) { + ErrorDialog( + content = "Please switch account first to navigate to the event.", + onSubmit = { state.eventSink(PushHistoryEvents.ClearDialog) } + ) + } } @Composable private fun PushHistoryContent( state: PushHistoryState, - onItemClick: (SessionId, RoomId, EventId) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -173,7 +175,7 @@ private fun PushHistoryContent( val roomId = pushHistory.roomId val eventId = pushHistory.eventId if (sessionId != null && roomId != null && eventId != null) { - onItemClick(sessionId, roomId, eventId) + state.eventSink(PushHistoryEvents.NavigateTo(sessionId, roomId, eventId)) } } ) @@ -271,6 +273,5 @@ internal fun PushHistoryViewPreview( PushHistoryView( state = state, onBackClick = {}, - onItemClick = { _, _, _ -> }, ) } diff --git a/libraries/troubleshoot/impl/src/main/res/values-bg/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..3af1921802 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,7 @@ + + + "Изпълняване на тестове" + "Изпълняване на тестовете отново" + "Всички тестове преминаха успешно." + "Отстраняване на неизправности с известията" + 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 88a99f0f66..60a11ba8de 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-de/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-de/translations.xml @@ -4,9 +4,9 @@ "Tests durchführen" "Tests erneut durchführen" "Einige Tests sind fehlgeschlagen. Bitte überprüfe die Details." - "Führen Sie Tests durch, um Probleme in Ihren Einstellungen zu erkennen, die zu fehlerhaften Benachrichtigungen führen könnten." + "Führe die Tests durch, um Probleme zu erkennen, die dazu führen können, dass sich die Benachrichtigungen nicht wie erwartet verhalten." "Problem beheben" "Alle Tests wurden erfolgreich bestanden." - "Beheben Sie die Fehler bei Benachrichtigungen" - "Einige Tests erfordern Ihre Aufmerksamkeit. Bitte überprüfen Sie die Angaben." + "Fehlerbehebung für Benachrichtigungen" + "Einige Tests erfordern deine Aufmerksamkeit. Bitte überprüfe die Details." 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 6f359ecb22..ff76ef3a44 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-hu/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-hu/translations.xml @@ -1,6 +1,6 @@ - "Leküldési értesítés előzmények" + "Leküldéses értesítések 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-ko/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..9713cae67e --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,12 @@ + + + "푸시 기록" + "테스트 실행" + "테스트를 다시 실행하세요" + "일부 테스트가 실패했습니다. 자세한 내용을 확인해 주세요." + "구성에서 알림이 예상대로 작동하지 않게 하는 문제가 있는지 감지하기 위해 테스트를 실행하세요." + "수정을 시도하다" + "모든 테스트를 성공적으로 통과했습니다." + "문제 해결 알림" + "일부 테스트는 귀하의 주의가 필요합니다. 자세한 내용을 확인해 주시기 바랍니다." + 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 6990ca5dbe..5b1e7a7791 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,11 +1,11 @@ "Histórico de push" - "Execute testes" - "Execute os testes novamente" + "Executar testes" + "Executar 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" + "Execute os testes para detectar qualquer problema na sua configuração que possa fazer com que as notificações não se comportem como esperado." + "Tentar consertar" "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 d82bbd2e15..807e29e123 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-pt/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-pt/translations.xml @@ -1,6 +1,6 @@ - "Histórico de notificações" + "Histórico de push" "Correr testes" "Correr testes novamente" "Alguns testes falharam. Por favor, verifica os detalhes." diff --git a/libraries/troubleshoot/impl/src/main/res/values-ro/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-ro/translations.xml index a10361672e..c28887c3c5 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-ro/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-ro/translations.xml @@ -1,5 +1,6 @@ + "Istoricul notificărilor" "Rulați testele" "Rulați din nou testele" "Unele teste au eșuat. Vă rugăm să verificați detaliile." diff --git a/libraries/troubleshoot/impl/src/main/res/values-uz/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-uz/translations.xml new file mode 100644 index 0000000000..e01e098f5f --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-uz/translations.xml @@ -0,0 +1,11 @@ + + + "Testlarni ishga tushirish" + "Testlarni qayta ishga tushirish" + "Ba’zi testlar muvaffaqiyatsiz tugadi. Iltimos, tafsilotlarni tekshirib chiqing." + "Sozlamalaringizdagi bildirishnomalarning kutilganidek ishlamasligi mumkin bo‘lgan har qanday muammoni aniqlash uchun testlarni o‘tkazing." + "Tuzatishga urinish" + "Barcha testlar muvaffaqiyatli yakunlandi." + "Bildirishnomalar bilan bog‘liq muammolarni bartaraf etish" + "Baʼzi testlar sizning eʼtiboringizni talab etadi. Iltimos, tafsilotlarni tekshirib chiqing." + diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/DefaultNotificationTroubleShootEntryPointTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/DefaultNotificationTroubleShootEntryPointTest.kt new file mode 100644 index 0000000000..e0d817a4ca --- /dev/null +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/DefaultNotificationTroubleShootEntryPointTest.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.troubleshoot.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint +import io.element.android.services.analytics.test.FakeScreenTracker +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultNotificationTroubleShootEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultNotificationTroubleShootEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + TroubleshootNotificationsNode( + buildContext = buildContext, + plugins = plugins, + factory = { createTroubleshootNotificationsPresenter() }, + screenTracker = FakeScreenTracker(), + ) + } + val callback = object : NotificationTroubleShootEntryPoint.Callback { + override fun onDone() = lambdaError() + override fun openIgnoredUsers() = lambdaError() + } + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .callback(callback) + .build() + assertThat(result).isInstanceOf(TroubleshootNotificationsNode::class.java) + assertThat(result.plugins).contains(callback) + } +} diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/FakeNotificationTroubleshootTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/FakeNotificationTroubleshootTest.kt index bfd4025a2a..7fa2e93d85 100644 --- a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/FakeNotificationTroubleshootTest.kt +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/FakeNotificationTroubleshootTest.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.troubleshoot.impl +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import kotlinx.coroutines.CoroutineScope @@ -50,7 +51,10 @@ class FakeNotificationTroubleshootTest( } } - override suspend fun quickFix(coroutineScope: CoroutineScope) { + override suspend fun quickFix( + coroutineScope: CoroutineScope, + navigator: NotificationTroubleshootNavigator, + ) { updateState(NotificationTroubleshootTestState.Status.InProgress) quickFixAction()?.let { _state.emit(it) diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenterTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenterTest.kt index 4b456fe494..4d6b338db7 100644 --- a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenterTest.kt +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenterTest.kt @@ -7,15 +7,15 @@ package io.element.android.libraries.troubleshoot.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.architecture.AsyncAction import io.element.android.libraries.push.test.FakeGetCurrentPushProvider +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.test import kotlinx.coroutines.test.runTest import org.junit.Test @@ -23,9 +23,7 @@ class TroubleshootNotificationsPresenterTest { @Test fun `present - initial state`() = runTest { val presenter = createTroubleshootNotificationsPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() assertThat(initialState.testSuiteState.tests).isEmpty() assertThat(initialState.testSuiteState.mainState).isEqualTo(AsyncAction.Uninitialized) @@ -40,9 +38,7 @@ class TroubleshootNotificationsPresenterTest { val presenter = createTroubleshootNotificationsPresenter( troubleshootTestSuite = troubleshootTestSuite, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() initialState.eventSink(TroubleshootNotificationsEvents.StartTests) skipItems(1) @@ -63,9 +59,7 @@ class TroubleshootNotificationsPresenterTest { val presenter = createTroubleshootNotificationsPresenter( troubleshootTestSuite = troubleshootTestSuite, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() initialState.eventSink(TroubleshootNotificationsEvents.RetryFailedTests) skipItems(1) @@ -74,6 +68,80 @@ class TroubleshootNotificationsPresenterTest { } } + @Test + fun `present - critical failed test`() { + `present - check main state`( + tests = setOf( + FakeNotificationTroubleshootTest( + firstStatus = NotificationTroubleshootTestState.Status.Failure(isCritical = true) + ) + ), + expectedIsCritical = true, + expectedMainState = AsyncAction.Failure::class.java, + ) + } + + @Test + fun `present - success and critical failed test`() { + `present - check main state`( + tests = setOf( + FakeNotificationTroubleshootTest( + firstStatus = NotificationTroubleshootTestState.Status.Success + ), + FakeNotificationTroubleshootTest( + firstStatus = NotificationTroubleshootTestState.Status.Failure(isCritical = true) + ), + ), + expectedIsCritical = true, + expectedMainState = AsyncAction.Failure::class.java, + ) + } + + @Test + fun `present - non critical failed test`() { + `present - check main state`( + tests = setOf( + FakeNotificationTroubleshootTest( + firstStatus = NotificationTroubleshootTestState.Status.Failure(isCritical = false) + ) + ), + expectedIsCritical = false, + expectedMainState = AsyncAction.Success::class.java, + ) + } + + @Test + fun `present - waiting for user`() { + `present - check main state`( + tests = setOf( + FakeNotificationTroubleshootTest( + firstStatus = NotificationTroubleshootTestState.Status.WaitingForUser + ) + ), + expectedIsCritical = false, + expectedMainState = AsyncAction.ConfirmingNoParams::class.java, + ) + } + + private fun `present - check main state`( + tests: Set, + expectedIsCritical: Boolean, + expectedMainState: Class>, + ) = runTest { + val troubleshootTestSuite = createTroubleshootTestSuite( + tests = tests + ) + val presenter = createTroubleshootNotificationsPresenter( + troubleshootTestSuite = troubleshootTestSuite, + ) + presenter.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.hasFailedTests).isEqualTo(expectedIsCritical) + assertThat(initialState.testSuiteState.mainState).isInstanceOf(expectedMainState) + } + } + @Test fun `present - quick fix test`() = runTest { val troubleshootTestSuite = createTroubleshootTestSuite( @@ -86,9 +154,7 @@ class TroubleshootNotificationsPresenterTest { val presenter = createTroubleshootNotificationsPresenter( troubleshootTestSuite = troubleshootTestSuite, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { skipItems(1) val initialState = awaitItem() assertThat(initialState.testSuiteState.mainState).isInstanceOf(AsyncAction.Failure::class.java) @@ -97,23 +163,27 @@ class TroubleshootNotificationsPresenterTest { assertThat(stateAfterStart.testSuiteState.mainState).isEqualTo(AsyncAction.Loading) } } - - private fun createTroubleshootTestSuite( - tests: Set = emptySet(), - currentPushProvider: String? = null, - ): TroubleshootTestSuite { - return TroubleshootTestSuite( - notificationTroubleshootTests = tests, - getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider), - analyticsService = FakeAnalyticsService(), - ) - } - - private fun createTroubleshootNotificationsPresenter( - troubleshootTestSuite: TroubleshootTestSuite = createTroubleshootTestSuite(), - ): TroubleshootNotificationsPresenter { - return TroubleshootNotificationsPresenter( - troubleshootTestSuite = troubleshootTestSuite, - ) - } +} + +private fun createTroubleshootTestSuite( + tests: Set = emptySet(), + currentPushProvider: String? = null, +): TroubleshootTestSuite { + return TroubleshootTestSuite( + notificationTroubleshootTests = tests, + getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider), + analyticsService = FakeAnalyticsService(), + ) +} + +internal fun createTroubleshootNotificationsPresenter( + navigator: NotificationTroubleshootNavigator = object : NotificationTroubleshootNavigator { + override fun openIgnoredUsers() = lambdaError() + }, + troubleshootTestSuite: TroubleshootTestSuite = createTroubleshootTestSuite(), +): TroubleshootNotificationsPresenter { + return TroubleshootNotificationsPresenter( + navigator = navigator, + troubleshootTestSuite = troubleshootTestSuite, + ) } diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/DefaultPushHistoryEntryPointTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/DefaultPushHistoryEntryPointTest.kt new file mode 100644 index 0000000000..858956488c --- /dev/null +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/DefaultPushHistoryEntryPointTest.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.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.google.common.truth.Truth.assertThat +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.FakeMatrixClient +import io.element.android.libraries.push.test.FakePushService +import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint +import io.element.android.services.analytics.test.FakeScreenTracker +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultPushHistoryEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultPushHistoryEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + PushHistoryNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { + PushHistoryPresenter( + pushHistoryNavigator = object : PushHistoryNavigator { + override fun navigateTo(roomId: RoomId, eventId: EventId) = lambdaError() + }, + pushService = FakePushService(), + matrixClient = FakeMatrixClient(), + ) + }, + screenTracker = FakeScreenTracker(), + ) + } + val callback = object : PushHistoryEntryPoint.Callback { + override fun onDone() = lambdaError() + override fun navigateTo(roomId: RoomId, eventId: EventId) = lambdaError() + } + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .callback(callback) + .build() + assertThat(result).isInstanceOf(PushHistoryNode::class.java) + assertThat(result.plugins).contains(callback) + } +} 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 index 313735b6e6..9c1cdee25c 100644 --- 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 @@ -11,9 +11,19 @@ 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.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.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID_2 +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.test.FakePushService +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 @@ -29,6 +39,7 @@ class PushHistoryPresenterTest { assertThat(initialState.pushHistoryItems).isEmpty() assertThat(initialState.showOnlyErrors).isFalse() assertThat(initialState.resetAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(initialState.showNotSameAccountError).isFalse() } } @@ -119,11 +130,57 @@ class PushHistoryPresenterTest { } } + @Test + fun `present - item click current account`() = runTest { + val pushHistoryNavigatorResult = lambdaRecorder { _, _ -> } + val presenter = createPushHistoryPresenter( + pushHistoryNavigator = { roomId, eventId -> + pushHistoryNavigatorResult(roomId, eventId) + } + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink( + PushHistoryEvents.NavigateTo( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + ) + ) + pushHistoryNavigatorResult.assertions() + .isCalledOnce() + .with(value(A_ROOM_ID), value(AN_EVENT_ID)) + } + } + + @Test + fun `present - item click not current account`() = runTest { + val presenter = createPushHistoryPresenter() + presenter.test { + val initialState = awaitItem() + initialState.eventSink( + PushHistoryEvents.NavigateTo( + sessionId = A_SESSION_ID_2, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + ) + ) + assertThat(awaitItem().showNotSameAccountError).isTrue() + // Reset error + initialState.eventSink(PushHistoryEvents.ClearDialog) + assertThat(awaitItem().showNotSameAccountError).isFalse() + } + } + private fun createPushHistoryPresenter( + pushHistoryNavigator: PushHistoryNavigator = PushHistoryNavigator { _, _ -> lambdaError() }, pushService: PushService = FakePushService(), + matrixClient: MatrixClient = FakeMatrixClient(), ): PushHistoryPresenter { return PushHistoryPresenter( + pushHistoryNavigator = pushHistoryNavigator, pushService = pushService, + matrixClient = matrixClient, ) } } diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt index 5c98b2c21a..ada8b61f35 100644 --- a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt @@ -14,20 +14,14 @@ 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 @@ -103,9 +97,8 @@ class PushHistoryViewTest { } @Test - fun `clicking on a valid event invokes the expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - val onItemClick = lambdaRecorder { _, _, _ -> } + fun `clicking on a valid event emits the expected Event`() { + val eventsRecorder = EventsRecorder() rule.setPushHistoryView( aPushHistoryState( pushHistoryItems = listOf( @@ -118,25 +111,26 @@ class PushHistoryViewTest { ), 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)) + eventsRecorder.assertSingle( + PushHistoryEvents.NavigateTo( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = 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/troubleshoot/test/build.gradle.kts b/libraries/troubleshoot/test/build.gradle.kts new file mode 100644 index 0000000000..830eb5d6b0 --- /dev/null +++ b/libraries/troubleshoot/test/build.gradle.kts @@ -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. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.troubleshoot.test" +} + +dependencies { + implementation(projects.libraries.troubleshoot.api) + implementation(projects.tests.testutils) + implementation(libs.coroutines.test) + implementation(libs.test.core) + implementation(libs.test.turbine) +} + +ktlint { + filter { + exclude { element -> + val path = element.file.path + // Exclude this file, that ktlint cannot parse. + path.contains("libraries/troubleshoot/test/src/main/kotlin/io/element/android/libraries/troubleshoot/test/Utils.kt") + } + } +} diff --git a/libraries/troubleshoot/test/src/main/kotlin/io/element/android/libraries/troubleshoot/test/FakeNotificationTroubleshootNavigator.kt b/libraries/troubleshoot/test/src/main/kotlin/io/element/android/libraries/troubleshoot/test/FakeNotificationTroubleshootNavigator.kt new file mode 100644 index 0000000000..63445e5a3e --- /dev/null +++ b/libraries/troubleshoot/test/src/main/kotlin/io/element/android/libraries/troubleshoot/test/FakeNotificationTroubleshootNavigator.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.troubleshoot.test + +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeNotificationTroubleshootNavigator( + private val openIgnoredUsersResult: () -> Unit = { lambdaError() }, +) : NotificationTroubleshootNavigator { + override fun openIgnoredUsers() = openIgnoredUsersResult() +} diff --git a/libraries/troubleshoot/test/src/main/kotlin/io/element/android/libraries/troubleshoot/test/Utils.kt b/libraries/troubleshoot/test/src/main/kotlin/io/element/android/libraries/troubleshoot/test/Utils.kt new file mode 100644 index 0000000000..77034da584 --- /dev/null +++ b/libraries/troubleshoot/test/src/main/kotlin/io/element/android/libraries/troubleshoot/test/Utils.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. + */ + +@file:Suppress("UnusedImports") + +package io.element.android.libraries.troubleshoot.test + +import app.cash.turbine.TurbineTestContext +import app.cash.turbine.test +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope + +context(testScope: TestScope) +suspend fun NotificationTroubleshootTest.runAndTestState( + validate: suspend TurbineTestContext.() -> Unit, +) { + testScope.backgroundScope.launch { + run(this) + } + state.test(validate = validate) +} diff --git a/libraries/ui-common/build.gradle.kts b/libraries/ui-common/build.gradle.kts new file mode 100644 index 0000000000..55ef7eaf49 --- /dev/null +++ b/libraries/ui-common/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.ui.common" +} + +dependencies { + implementation(libs.appyx.core) + implementation(projects.libraries.designsystem) +} diff --git a/libraries/ui-common/src/main/kotlin/io/element/android/libraries/ui/common/nodes/EmptyNode.kt b/libraries/ui-common/src/main/kotlin/io/element/android/libraries/ui/common/nodes/EmptyNode.kt new file mode 100644 index 0000000000..ab622b4b9d --- /dev/null +++ b/libraries/ui-common/src/main/kotlin/io/element/android/libraries/ui/common/nodes/EmptyNode.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.libraries.ui.common.nodes + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.node +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight + +/** + * Ref: https://www.figma.com/design/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=1518-85323 + */ +fun emptyNode( + buildContext: BuildContext, +): Node = node(buildContext) { modifier -> + EmptyView(modifier) +} + +@Composable +private fun EmptyView( + modifier: Modifier = Modifier, +) = Box( + modifier = modifier + .fillMaxSize() + .background(ElementTheme.colors.bgCanvasDefault), +) + +@PreviewsDayNight +@Composable +internal fun EmptyViewPreview() = ElementPreview { + EmptyView(Modifier) +} 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 153476931b..57b3a43d1a 100644 --- a/libraries/ui-strings/src/main/res/values-bg/translations.xml +++ b/libraries/ui-strings/src/main/res/values-bg/translations.xml @@ -1,14 +1,21 @@ + "Добавяне на реакция: %1$s" "Изтриване" "%1$d въведена цифра" "%1$d въведени цифри" + "Пълният адрес ще бъде %1$s" + "Подробности за шифроването" "Скриване на паролата" + "Присъединяване към обаждане" "Скок към най-долу" "Само споменавания" "Заглушено" + "Нови споменавания" + "Нови съобщения" + "Текущо обаждане" "Страница %1$d" "Пауза" "PIN поле" @@ -26,16 +33,20 @@ "Премахване на реакция с %1$s" "Изпращане на файлове" "Показване на паролата" + "Започнете обаждане" "Потребителско меню" "Вижте подробности" "Приемане" + "Добавяне към хронологията" "Назад" + "Обаждане" "Отказ" "Избор на снимка" "Изчистване" "Затваряне" "Завършване на потвърждаването" "Потвърждаване" + "Потвърдете паролата" "Продължаване" "Копиране" "Копиране на връзката" @@ -68,6 +79,7 @@ "Напускане" "Напускане на разговора" "Напускане на стаята" + "Напускане на пространството" "Зареждане на още" "Управление на профила" "Управление на устройствата" @@ -85,7 +97,11 @@ "Премахване на съобщението" "Отговор" "Отговор в нишка" + "Докладване" + "Докладване на грешка" "Докладване на съдържанието" + "Докладване на стаята" + "Нулиране" "Повторен опит" "Повторен опит за разшифроване" "Запазване" @@ -102,25 +118,35 @@ "Започване" "Започване на чат" "Започване на потвърждаването" + "Докоснете за зареждане на карта" "Снимка" "Докоснете за опции" "Повторен опит" + "Откачване" "Преглед на източника" "Да" "Да, опитай отново" "Относно" + "Политика за приемлива употреба" + "Добавяне на акаунт" "Разширени настройки" "Статистика" + "Напуснахте стаята" "Облик" "Аудио" "Блокирани потребители" + "Мехурчета" + "Започнато обаждане" "Резервно копие на чатовете" + "Авторски права" "Създаване на стая…" "Тъмен" "Грешка при разшифроване" + "Описание" "Опции за разработчици" "Директен чат" "Не показвай това отново" + "Неуспешно изтегляне" "Изтегля се" "(редактирано)" "Редактиране" @@ -132,18 +158,22 @@ "Грешка" "Всеки" "Фаворизиране" + "Фаворизирано" "Файл" "Файлът е изтрит" "Файлът е запазен" + "Файлът е запазен в Изтеглени" "Препращане на съобщението" "GIF" "Изображение" "В отговор на %1$s" "Инсталиране на APK" + "Този Matrix ID не може да бъде намерен, така че поканата може да не бъде получена." "Стаята се напуска" "Светъл" "Връзката е копирана в клипборда" "Зарежда се…" + "Зарежда се още…" "%d друг" "%d други" @@ -158,13 +188,18 @@ "Модерно" "Заглушаване" "Няма резултати" + "Няма име на стая" + "Няма име на пространство" "Без шифроване" "Офлайн" + "Лицензи за отворен код" "или" "Парола" "Хора" "Постоянна връзка" "Разрешение" + "Закачено" + "Моля, проверете вашата интернет връзка" "Моля, изчакайте…" "Сигурни ли сте, че искате да приключите тази анкета?" "Анкета: %1$s" @@ -174,19 +209,32 @@ "%d глас" "%d гласа" + "Подготвя се…" "Политика за поверителност" "Частна стая" "Общодостъпна стая" + "Общодостъпно пространство" "Реакция" "Реакции" "Причина" "Ключ за възстановяване" + "Опреснява се…" + + "%1$d отговор" + "%1$d отговора" + + "В отговор на %1$s" "Съобщаване за грешка" "Съобщаване за проблем" + "Докладът е изпратен" "Редактор на богат текст" "Стая" "Име на стаята" "напр. името на вашия проект" + + "%1$d стая" + "%1$d стаи" + "Запазва се" "Заключване на екрана" "Търсене на някого" @@ -197,20 +245,28 @@ "Изпращането е неуспешно" "Изпратено" "Сървърът не се поддържа" + "URL адрес на сървъра" "Настройки" "Споделено местоположение" "Излизате" "Възникна проблем. Моля, опитайте отново." + + "%1$d пространство" + "%1$d пространства" + "Започване на чат…" + "Стикер" "Успешно" "Предложения" "Синхронизиране" "Система" "Текст" + "Уведомления от трети страни" "Нишка" "Тема" "За какво се отнася тази стая?" "Не може да се разшифрова" + "Поканите не можаха да бъдат изпратени до един или повече потребители." "Не може да се изпрати покана(и)" "Отключване" "Раззаглушаване" @@ -225,16 +281,35 @@ "Потвърждаване на потребителя" "Видео" "Гласово съобщение" + "Изчаква се…" "В очакване на това съобщение" "Вие" + "Потвърждение" "Грешка" "Успешно" "Внимание" + "Неуспешно създаване на постоянна връзка" + "%1$s не успя да зареди картата. Моля, опитайте отново по-късно." + "Неуспешно зареждане на съобщения" + "%1$s няма достъп до вашето местоположение. Моля, опитайте отново по-късно." + "%1$s няма разрешение за достъп до вашето местоположение. Можете да активирате достъпа в Настройки." + "%1$s няма разрешение за достъп до вашето местоположение. Активирайте достъпа по-долу." + "Някои съобщения не са изпратени" + "Съжаляваме, възникна грешка" "Без шифроване" "🔐️ Присъединете се към мен в %1$s" "Хей, говорете с мен в %1$s: %2$s" "%1$s Android" + "Неуспешен избор на мултимедия, моля, опитайте отново." + "Натиснете върху съобщение и изберете „%1$s“, за да го включите тук." + "Закачете важни съобщения, за да могат лесно да бъдат намерени" + + "%1$d закачено съобщение" + "%1$d закачени съобщения" + "Закачени съобщения" + "Неуспешна обработка на мултимедия за качване, моля, опитайте отново." + "Не могат да бъдат извлечени потребителските данни" "Споделяне на местоположение" "Споделяне на моето местоположение" "Отваряне в Apple Maps" 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 5a67d92b4f..1b4d91547d 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -2,6 +2,7 @@ "Přidat reakci: %1$s" "Profilový obrázek" + "Minimalizovat textové pole zprávy" "Smazat" "zadána %1$d číslice" @@ -11,6 +12,7 @@ "Upravit avatar" "Úplná adresa bude %1$s" "Podrobnosti o šifrování" + "Rozbalit textové pole zprávy" "Skrýt heslo" "Připojit se k hovoru" "Přejít dolů" @@ -42,9 +44,9 @@ "Odstranit reakci pomocí %1$s" "Avatar místnosti" "Odeslat soubory" + "Vyžaduje se časově omezená akce, na ověření máte jednu minutu" "Zobrazit heslo" "Zahájit hovor" - "Vyžaduje se časově omezená akce" "Místnost s náhrobkem" "Avatar uživatele" "Uživatelské menu" @@ -90,6 +92,7 @@ "Povolit" "Ukončit hlasování" "Zadejte PIN" + "Dokončit" "Zapomněli jste heslo?" "Přeposlat" "Přejít zpět" @@ -104,6 +107,7 @@ "Odejít" "Opustit konverzaci" "Opustit místnost" + "Opustit prostor" "Načíst více" "Spravovat účet" "Spravovat zařízení" @@ -164,10 +168,14 @@ "Upgrade k dispozici" "O aplikaci" "Zásady používání" + "Přidat účet" + "Přidat další účet" "Přidání titulku" "Pokročilá nastavení" "obrázek" "Analytika" + "Opustili jste místnost" + "Byli jste odhlášeni z relace" "Vzhled" "Zvuk" "Blokovaní uživatelé" @@ -179,9 +187,12 @@ "Vytváření místnosti…" "Žádost zrušena" "Místnost opuštěna" + "Opustit prostor" "Pozvánka odmítnuta" "Tmavé" "Chyba dešifrování" + "Popis" + "Odznačit vše" "Možnosti pro vývojáře" "ID zařízení" "Přímý chat" @@ -240,6 +251,7 @@ Důvod: %1$s." "%1$s (%2$s)" "Žádné výsledky" "Žádný název místnosti" + "Žádný název prostoru" "Nešifrováno" "Offline" "Licence s otevřeným zdrojovým kódem" @@ -260,9 +272,12 @@ Důvod: %1$s." "%d hlasy" "%d hlasů" + "Příprava…" "Zásady ochrany osobních údajů" "Soukromá místnost" + "Soukromý prostor" "Veřejná místnost" + "Veřejný prostor" "Reakce" "Reakce" "Důvod" @@ -279,6 +294,11 @@ Důvod: %1$s." "Místnost" "Název místnosti" "např. název vašeho projektu" + + "%1$d místnost" + "%1$d místnosti" + "%1$d místností" + "Uložené změny" "Ukládání" "Zámek obrazovky" @@ -286,18 +306,28 @@ Důvod: %1$s." "Výsledky hledání" "Zabezpečení" "Viděno" + "Vybrat účet" + "Vybrat vše" "Odeslat do" "Odesílání…" "Odeslání se nezdařilo" "Odesláno" ". " "Server není podporován" + "Server je nedostupný" "URL serveru" "Nastavení" + "Sdílet prostor" "Sdílená poloha" "Odhlašování" "Něco se nepovedlo" "Narazili jsme na problém. Zkuste to prosím znovu." + "Prostor" + + "%1$d prostor" + "%1$d prostory" + "%1$d prostorů" + "Zahajování chatu…" "Nálepka" "Úspěch" @@ -328,6 +358,12 @@ Důvod: %1$s." "Ověření identity" "Ověřit uživatele" "Video" + "Vysoká kvalita" + "Nejlepší kvalita, ale větší velikost souboru" + "Nízká kvalita" + "Nejrychlejší rychlost nahrávání a nejmenší velikost souboru" + "Standardní kvalita" + "Rovnováha mezi kvalitou a rychlostí nahrávání" "Hlasová zpráva" "Čekání…" "Čekání na dešifrovací klíč" @@ -342,6 +378,10 @@ Důvod: %1$s." Opravdu chcete pokračovat?" "Zkontrolujte tento odkaz" + "Vyberte výchozí kvalitu nahrávaných videí." + "Kvalita nahrávání videa" + "Maximální povolená velikost souboru je: %1$s" + "Soubor je pro nahrání příliš velký." "Místnost nahlášena" "Nahlášen a opustil místnost" "Potvrzení" @@ -350,6 +390,11 @@ Opravdu chcete pokračovat?" "Upozornění" "Vaše změny nebyly uloženy. Opravdu se chcete vrátit?" "Uložit změny?" + "Maximální povolená velikost souboru je: %1$s" + "Vyberte kvalitu videa, které chcete nahrát." + "Vyberte kvalitu nahrávání videa" + "Hledat emotikony" + "Na tomto zařízení jste již přihlášeni jako %1$s." "Váš domovský server je třeba upgradovat, aby podporoval službu Matrix Authentication Service a vytváření účtu." "Vytvoření trvalého odkazu se nezdařilo" "%1$s nemohl načíst mapu. Zkuste to prosím později." @@ -401,6 +446,7 @@ Opravdu chcete pokračovat?" "Vaše zpráva nebyla odeslána, protože%1$s neověřil(a) všechna zařízení" "Jedno nebo více vašich zařízení není ověřeno. Zprávu můžete přesto odeslat, nebo ji můžete prozatím zrušit a zkusit to znovu později, až ověříte všechna svá zařízení." "Vaše zpráva nebyla odeslána, protože jste neověřili jedno nebo více zařízení" + "Upravit správce nebo vlastníky" "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" @@ -418,6 +464,10 @@ Opravdu chcete pokračovat?" "Otevřít v Mapách Google" "Otevřít v OpenStreetMap" "Sdílet tuto polohu" + "Prostory, které jste vytvořili nebo se k nim připojili." + "%1$s • %2$s" + "%1$s prostor" + "Prostory" "Zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila." "Zpráva nebyla odeslána, protože%1$s neověřil(a) všechna zařízení." "Zpráva nebyla odeslána, protože jste neověřili jedno nebo více zařízení." diff --git a/libraries/ui-strings/src/main/res/values-cy/translations.xml b/libraries/ui-strings/src/main/res/values-cy/translations.xml index bf4c9a67c6..6c0788070f 100644 --- a/libraries/ui-strings/src/main/res/values-cy/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cy/translations.xml @@ -2,6 +2,7 @@ "Ychwanegu adwaith: %1$s" "Afatar" + "Lleihau maes testun neges" "Dileu" "%1$d nodau wedi eu cynnig" @@ -12,10 +13,13 @@ "%1$d nod wedi eu cynnig" "Golygu afatar" + "Bydd y cyfeiriad llawn yn%1$s" "Manylion amgryptio" + "Ehangu maes testun neges" "Cuddio cyfrinair" "Ymuno â galwad" "Symud i\'r gwaelod" + "Symud y map i\'m lleoliad" "Crybwylliadau\'n unig" "Wedi\'i Dewi" "Crybwylliadau newydd" @@ -46,9 +50,10 @@ "Wedi dileu adwaith gyda %1$s" "Afatar ystafell" "Anfon ffeiliau" + "Mae angen gweithredu o fewn amser cyfyngedig, mae gennych un funud i wirio" "Dangos y cyfrinair" "Cychwyn galwad" - "Mae angen gweithredu â chyfyngiad amser" + "Ystafell Tombstoned" "Afatar defnyddiwr" "Dewislen defnyddiwr" "Gweld afatar" @@ -93,6 +98,7 @@ "Galluogi" "Gorffen pleidlais" "Rhoi\'r PIN" + "Gorffen" "Wedi anghofio\'ch cyfrinair?" "Ymlaen" "Mynd nôl" @@ -107,6 +113,7 @@ "Gadael" "Gadael y sgwrs" "Gadael yr ystafell" + "Gadael y gofod" "Llwytho rhagor" "Rheoli cyfrif" "Rheoli dyfeisiau" @@ -124,8 +131,8 @@ "Ymateb" "Gwrthod" "Tynnu" - "Dileu capsiwn" - "Dileu neges" + "Tynnu capsiwn" + "Tynnu neges" "Ateb" "Ateb mewn edefyn" "Adroddiadau" @@ -167,10 +174,14 @@ "Uwchraddiad ar gael" "Ynghylch" "Polisi defnydd derbyniol" + "Ychwanegu cyfrif" + "Ychwanegu cyfrif arall" "Ychwanegu capsiwn" "Gosodiadau uwch" "delwedd" "Dadansoddeg" + "Rydych wedi gadael yr ystafell" + "Rydych wedi\'ch allgofnodi o\'r sesiwn" "Gwedd" "Sain" "Defnyddwyr wedi\'u rhwystro" @@ -182,9 +193,11 @@ "Wrthi\'n creu ystafell…" "Cais wedi\'i ddiddymu" "Wedi gadael yr ystafell" + "Gofod chwith" "Wedi gwrthod y gwahoddiad" "Tywyll" "Gwall dadgryptio" + "Disgrifiad" "Dewisiadau datblygwr" "ID dyfais" "Sgwrs uniongyrchol" @@ -233,12 +246,12 @@ Rheswm: %1$s." "%d arall" - "%1$d aelodau" - "%1$d aelod" - "%1$d aelod" - "%1$d aelod" - "%1$d aelod" - "%1$d aelod" + "%1$d Aelodau" + "%1$d Aelod" + "%1$d Aelod" + "%1$d Aelod" + "%1$d Aelod" + "%1$d Aelod" "Neges" "Gweithredoedd neges" @@ -272,14 +285,25 @@ Rheswm: %1$s." "%d pleidlais" "%d pleidlais" + "Yn paratoi…" "Polisi preifatrwydd" "Ystafell breifat" + "Gofod preifat" "Ystafell gyhoeddus" + "Gofod cyhoeddus" "Adwaith" "Adweithiau" "Rheswm" "Allwedd adfer" "Wrthi\'n adnewyddu…" + + "%1$d atebion" + "%1$d ateb" + "%1$d ateb" + "%1$d ateb" + "%1$d ateb" + "%1$d ateb" + "Yn ymateb i %1$s" "Adrodd ar wall" "Adrodd am broblem" @@ -288,6 +312,14 @@ Rheswm: %1$s." "Ystafell" "Enw\'r ystafell" "e.e. enw eich project" + + "%1$d Ystafelloedd" + "%1$d Ystafell" + "%1$d Ystafell" + "%1$d Ystafell" + "%1$d Ystafell" + "%1$d Ystafell" + "Newidiadau wedi\'u cadw" "Cadw" "Clo sgrin" @@ -295,18 +327,30 @@ Rheswm: %1$s." "Canlyniadau chwilio" "Diogelwch" "Wedi\'i weld gan" + "Dewis cyfrif" "Anfon at" "Yn anfon…" "Methodd anfon" "Anfonwyd" ". " "Nid yw\'r gweinydd yn cael ei gynnal" + "Gweinydd yn anghyraeddadwy" "URL gweinydd" "Gosodiadau" + "Rhannu gofod" "Lleoliad yn cael ei rannu" "Allgofnodi" "Aeth rhywbeth o\'i le" "Wedi canfod mater. Ceisiwch eto." + "Gofod" + + "%1$d Gofodau" + "%1$d Gofod" + "%1$d Ofod" + "%1$d Gofod" + "%1$d Gofod" + "%1$d Gofod" + "Dechrau sgwrs…" "Sticer" "Llwyddiant" @@ -337,6 +381,12 @@ Rheswm: %1$s." "Gwirio hunaniaeth" "Gwirio defnyddiwr" "Fideo" + "Ansawdd uchel" + "Yr ansawdd gorau ond maint ffeil mwy" + "Ansawdd isel" + "Y cyflymder llwytho cyflymaf a\'r maint ffeil lleiaf" + "Ansawdd safonol" + "Cydbwysedd ansawdd a chyflymder llwytho" "Neges llais" "Yn aros…" "Yn aros am y neges hon" @@ -351,6 +401,10 @@ Rheswm: %1$s." Ydych chi\'n siŵr eich bod am barhau?" "Gwnewch yn siŵr fod y ddolen hon yn iawn" + "Dewiswch ansawdd rhagosodedig y fideos rydych chi\'n eu llwytho." + "Ansawdd llwytho fideo" + "Y maint ffeil mwyaf sy\'n cael ei ganiatáu yw:%1$s" + "Mae maint y ffeil yn rhy fawr i\'w llwytho" "Adroddwyd am yr ystafell" "Adroddwyd a gadael yr ystafell" "Cadarnhad" @@ -359,6 +413,11 @@ Ydych chi\'n siŵr eich bod am barhau?" "Rhybudd" "Dyw eich newidiadau heb gael eu cadw. Ydych chi\'n siŵr eich bod am fynd nôl?" "Cadw\'r newidiadau?" + "Y maint ffeil mwyaf sy\'n cael ei ganiatáu yw: %1$s" + "Dewiswch ansawdd y fideo rydych chi am ei llwytho." + "Dewiswch ansawdd llwytho fideo" + "Chwilio emojis" + "Rydych chi eisoes wedi mewngofnodi ar y ddyfais hon fel %1$s ." "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." @@ -386,6 +445,7 @@ Ydych chi\'n siŵr eich bod am barhau?" "Hei, siaradwch â mi ar %1$s: %2$s" "Android %1$s" "Rageshake i adrodd gwall" + "Llun sgrin" "%1$s: %2$s" "Dewisiadau" "Tynnu %1$s" @@ -412,6 +472,7 @@ Ydych chi\'n siŵr eich bod am barhau?" "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" + "Golygu Gweinyddwyr neu Berchnogion" "Wedi methu â phrosesu cyfryngau i\'w llwytho, ceisiwch eto." "Methu â nôl manylion defnyddiwr" "Neges yn %1$s" @@ -429,6 +490,9 @@ Ydych chi\'n siŵr eich bod am barhau?" "Agor yn Google Maps" "Agor yn OpenStreetMap" "Rhannu\'r lleoliad hwn" + "Gofodau rydych wedi\'u creu neu wedi ymuno â nhw." + "%1$s • %2$s" + "Gofodau" "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." diff --git a/libraries/ui-strings/src/main/res/values-da/translations.xml b/libraries/ui-strings/src/main/res/values-da/translations.xml index 2d5dd87645..d411d3d0d1 100644 --- a/libraries/ui-strings/src/main/res/values-da/translations.xml +++ b/libraries/ui-strings/src/main/res/values-da/translations.xml @@ -2,6 +2,7 @@ "Tilføj reaktion: %1$s" "Avatar" + "Minimér tekstfeltet for beskeder" "Slet" "%1$d ciffer indtastet" @@ -10,6 +11,7 @@ "Redigér avatar" "Den fulde adresse vil være %1$s" "Krypteringsoplysninger" + "Udvid tekstfeltet for beskeder" "Skjul adgangskode" "Deltag i opkald" "Hop til bunden" @@ -40,9 +42,9 @@ "Fjern reaktion med %1$s" "Avatar for rummet" "Send filer" + "Tidsbegrænset handling påkrævet, du har et minut til at bekræfte" "Vis adgangskode" "Start et opkald" - "Tidsbegrænset handling påkrævet" "Deaktiveret rum" "Avatar for bruger" "Brugermenu" @@ -69,7 +71,7 @@ "Kopiér" "Kopiér billedtekst" "Kopiér link" - "Kopier link til besked" + "Kopiér link til besked" "Kopiér tekst" "Opret" "Opret et rum" @@ -88,20 +90,22 @@ "Slå til" "Afslut afstemning" "Indtast PIN-kode" + "Afslut" "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" + "Invitér andre" + "Invitér andre til %1$s" + "Invitér andre til %1$s" "Invitationer" "Deltag" "Få mere at vide" "Forlad" "Forlad samtalen" "Forlad rum" + "Forlad klynge" "Indlæs mere" "Administrer konto" "Administrer enheder" @@ -162,10 +166,14 @@ "Opgradering tilgængelig" "Om" "Politik for acceptabel brug" + "Tilføj en konto" + "Tilføj en anden konto" "Tilføjelse af billedtekst" "Avancerede indstillinger" "et billede" "Analyse-værktøj" + "Du forlod rummet" + "Du blev logget ud af sessionen" "Udseende" "Lyd" "Blokerede brugere" @@ -177,9 +185,12 @@ "Opretter rum…" "Anmodning annulleret" "Forlod rummet" + "Forlod klynge" "Invitationen blev afvist" "Mørkt tema" "Fejl under dekryptering" + "Beskrivelse" + "Fravælg alle" "Indstillinger for udviklere" "Enheds-ID" "Direkte samtale" @@ -236,12 +247,13 @@ "%1$s (%2$s)" "Ingen resultater" "Intet rumnavn" + "Intet klyngenavn" "Ikke krypteret" "Offline" "Open Source-licenser" "eller" "Adgangskode" - "Mennesker" + "Brugere" "Permalink" "Tilladelse" "Fastgjort" @@ -288,18 +300,23 @@ "Søgeresultater" "Sikkerhed" "Set af" + "Vælg en konto" + "Vælg alle" "Send til" "Sender…" "Afsendelse mislykkedes" "Sendt" ". " "Serveren er ikke understøttet" + "Serveren er ikke tilgængelig" "Server URL" "Indstillinger" + "Del klynge" "Delt placering" "Logger ud" "Noget gik galt" "Vi stødte på et problem. Prøv venligst igen." + "Klynge" "%1$d Klynge" "%1$d Klynger" @@ -369,6 +386,8 @@ Er du sikker på, at du vil fortsætte?" "Den maksimalt tilladte filstørrelse er: %1$s" "Vælg den kvalitet, du ønsker at uploade videoen i." "Vælg kvalitet for video-overførsel" + "Søg emojis" + "Du er allerede logget ind på denne enhed som %1$s." "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." @@ -439,6 +458,7 @@ Er du sikker på, at du vil fortsætte?" "Del denne lokation" "Klynger, du har oprettet eller deltager i" "%1$s•%2$s" + "%1$s klynge" "Klynger" "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." 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 3bd2cd518a..a5557b2b60 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -2,16 +2,20 @@ "Reaktion hinzufügen: %1$s" "Avatar" + "Nachrichtentextfeld minimieren" "Löschen" "%1$d eingegebene Ziffer" "%1$d eingegebene Ziffern" "Avatar bearbeiten" + "Die vollständige Adresse lautet %1$s" "Details zur Verschlüsselung" - "Passwort verbergen" + "Nachrichtentextfeld vergrößern" + "Passwort ausblenden" "Anruf beitreten" "Nach unten springen" + "Verschiebe die Karte zu meinem Standort." "Nur Erwähnungen" "Stummgeschaltet" "Neue Erwähnungen" @@ -29,20 +33,21 @@ "Mit anderen Emojis reagieren" "Gelesen von %1$s und %2$s" - "Gelesen von %1$s und %2$d anderen" - "Gelesen von %1$s und %2$d anderen" + "Gelesen von %1$s und %2$d weiteren" + "Gelesen von %1$s und %2$d weiteren" "Gelesen von %1$s" "Tippe, um alle anzuzeigen" "Reaktion mit %1$s entfernen" "Entferne Reaktionen mit %1$s" - "Raum-Avatar" + "Avatar" "Dateien senden" + "Zeitlich begrenzte Handlung erforderlich, du hast eine Minute Zeit zur Verifizierung" "Passwort anzeigen" "Anruf starten" - "Zeitlich begrenztes Handeln erforderlich" - "Benutzer-Avatar" - "Benutzermenü" + "Stillgelegter Chat" + "Nutzer-Avatar" + "Nutzer-Menü" "Avatar ansehen" "Details anzeigen" "Sprachnachricht, Dauer: %1$s" @@ -69,7 +74,7 @@ "Link zur Nachricht kopieren" "Text kopieren" "Erstellen" - "Raum erstellen" + "Chat erstellen" "Deaktivieren" "Nutzerkonto deaktivieren" "Ablehnen" @@ -85,6 +90,7 @@ "Aktivieren" "Umfrage beenden" "PIN eingeben" + "Fertigstellen" "Passwort vergessen?" "Weiterleiten" "Zurück" @@ -99,6 +105,7 @@ "Verlassen" "Unterhaltung verlassen" "Verlassen" + "Space verlassen" "Mehr laden…" "Konto verwalten" "Geräte verwalten" @@ -117,14 +124,14 @@ "Ablehnen" "Entfernen" "Bildunterschrift entfernen" - "Nachricht löschen" + "Nachricht entfernen" "Antworten" "Im Thread antworten" "Bericht" "Fehler melden" "Inhalt melden" "Konversation melden" - "Chatroom melden" + "Chat melden" "Zurücksetzen" "Identität zurücksetzen" "Erneut versuchen" @@ -154,15 +161,19 @@ "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." + "Ja, versuche es noch einmal" + "Dein Homeserver unterstützt jetzt ein neues, schnelleres Protokoll. Melde dich ab und wieder an, um zu aktualisieren. Wenn du das jetzt tust, vermeidest du eine erzwungene Abmeldung, wenn das alte Protokoll später entfernt wird." "Aktualisierung verfügbar" "Über" "Nutzungsrichtlinie" + "Konto hinzufügen" + "Weiteres Konto hinzufügen" "Hinzufügen einer Bildunterschrift" "Erweiterte Einstellungen" "ein Bild" "Analysedaten" + "Du hast den Chat verlassen" + "Du wurdest aus der Sitzung abgemeldet." "Erscheinungsbild" "Audio" "Blockierte Nutzer" @@ -171,12 +182,15 @@ "Chat-Backup" "In die Zwischenablage kopiert" "Copyright" - "Raum wird erstellt…" - "Anfrage storniert" - "Hat den Raum verlassen" + "Chat wird erstellt…" + "Anfrage abgebrochen" + "Hat den Chat verlassen" + "Space verlassen" "Einladung abgelehnt" "Dunkel" "Dekodierungsfehler" + "Beschreibung" + "Auswahl aufheben" "Entwickleroptionen" "Geräte-ID" "Direktnachricht" @@ -192,13 +206,13 @@ "Verschlüsselung aktiviert" "PIN eingeben" "Fehler" - "Es ist ein Fehler aufgetreten. Sie erhalten eventuell keine Benachrichtigungen für neue Nachrichten. Bitte diagnostizieren Sie das Benachrichtigungsproblem in den Einstellungen. + "Es ist ein Fehler aufgetreten. Du erhältst eventuell keine Benachrichtigungen für neue Nachrichten. Bitte behebe den Fehler in den Einstellungen. Grund: %1$s." "Alle" "Fehlgeschlagen" "Favorit" - "Favorit" + "Favorisiert" "Datei" "Datei wurde gelöscht" "Datei gespeichert" @@ -210,7 +224,7 @@ Grund: %1$s." "Als Antwort auf %1$s" "APK installieren" "Diese Matrix Kennung wurde nicht gefunden, daher wird die Einladung möglicherweise nicht empfangen." - "Raum verlassen" + "Chat verlassen" "Hell" "Zeile in die Zwischenablage kopiert" "Link in die Zwischenablage kopiert" @@ -232,7 +246,8 @@ Grund: %1$s." "Stumm" "%1$s(%2$s)" "Keine Ergebnisse" - "Kein Raumname" + "Kein Chat-Name" + "Kein Space Name" "Nicht verschlüsselt" "Offline" "Open-Source-Lizenzen" @@ -240,11 +255,11 @@ Grund: %1$s." "Passwort" "Personen" "Permalink" - "Erlaubnis" + "Berechtigung" "Fixiert" - "Bitte überprüfen Sie Ihre Internetverbindung" + "Bitte überprüfe deine Internetverbindung" "Bitte warten…" - "Sind Sie sicher, dass Sie diese Umfrage beenden möchten?" + "Bist du sicher, dass du diese Umfrage beenden möchtest?" "Umfrage: %1$s" "Stimmen insgesamt: %1$s" "Ergebnisse werden nach Ende der Umfrage angezeigt" @@ -252,9 +267,12 @@ Grund: %1$s." "%d Stimme" "%d Stimmen" + "Vorbereitung läuft …" "Datenschutz­erklärung" - "Privater Chatroom" - "Öffentlicher Raum" + "Privater Chat" + "Privater Space" + "Öffentlicher Chat" + "Öffentlicher Space" "Reaktion" "Reaktionen" "Grund" @@ -269,9 +287,13 @@ Grund: %1$s." "Ein Problem melden" "Bericht eingereicht" "Rich-Text-Editor" - "Raum" - "Raumname" + "Chat" + "Chat-Name" "z.B. dein Projektname" + + "%1$d Chat" + "%1$d Chats" + "Gespeicherte Änderungen" "Speichern" "Bildschirmsperre" @@ -279,18 +301,27 @@ Grund: %1$s." "Suchergebnisse" "Sicherheit" "Gesehen von" + "Konto auswählen" + "Alles auswählen" "Senden an" "Wird gesendet…" "Senden fehlgeschlagen" "Gesendet" ". " "Server wird nicht unterstützt" + "Server nicht erreichbar" "Server-URL" "Einstellungen" + "Space teilen" "Geteilter Standort" "Abmelden" "Es ist ein Fehler aufgetreten." - "Wir sind auf ein Problem gestoßen. Bitte versuchen Sie es erneut." + "Wir haben ein Problem festgestellt. Bitte versuch es erneut." + "Space" + + "%1$d Space" + "%1$d Spaces" + "Chat wird gestartet…" "Sticker" "Erfolg" @@ -301,26 +332,32 @@ Grund: %1$s." "Hinweise von Drittanbietern" "Thread" "Thema" - "Worum geht es in diesem Raum?" + "Worum geht es in diesem Chat?" "Entschlüsselung nicht möglich" "Von einem ungesicherten Gerät gesendet" - "Sie haben keinen Zugriff auf diese Nachricht" + "Du hast 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." + "Einladungen konnten nicht an einen oder mehrere Nutzer gesendet werden." "Einladung(en) konnte(n) nicht gesendet werden" "Entsperren" "Stummschaltung aufheben" "Anruf nicht unterstützt" "Nicht unterstütztes Ereignis" - "Benutzername" + "Nutzername" "Verifizierung abgebrochen" "Verifizierung abgeschlossen" "Verifizierung fehlgeschlagen" "Verifiziert" "Gerät verifizieren" - "Identität überprüfen" - "Benutzer verifizieren" + "Identität verifizieren" + "Nutzer verifizieren" "Video" + "Hohe Qualität" + "Beste Qualität, aber größere Dateigröße" + "Niedrige Qualität" + "Schnellste Upload-Geschwindigkeit und kleinste Dateigröße" + "Standardqualität" + "Balance zwischen Qualität und Upload-Geschwindigkeit" "Sprachnachricht" "Warten…" "Warte auf diese Nachricht" @@ -331,31 +368,40 @@ Grund: %1$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. + "Der Link %1$s führt dich zu einer anderen Seite %2$s. -Möchten Sie wirklich fortfahren?" - "Überprüfen Sie diesen Link noch einmal" - "Chatroom gemeldet" - "Gemeldet und Zimmer verlassen" +Möchtest du wirklich fortfahren?" + "Überprüfe diesen Link noch einmal" + "Wähle die Standardqualität für Videos, die du hochlädst." + "Video-Upload-Qualität" + "Die maximal erlaubte Dateigröße ist: %1$s" + "Die Datei ist zu groß zum Hochladen." + "Chat gemeldet" + "Gemeldet und Chat verlassen" "Bestätigung" "Fehler" "Erfolg" "Warnung" - "Ihre Änderungen wurden nicht gespeichert. Sind Sie sicher, dass Sie zurückgehen wollen?" + "Deine Änderungen wurden nicht gespeichert. Bist du sicher, dass du zurückgehen willst?" "Änderungen speichern?" + "Die maximal erlaubte Dateigröße ist: %1$s" + "Wähle die Qualität des Videos, das du hochladen möchtest." + "Wähle die Video-Upload-Qualität" + "Emojis suchen" + "Du bist auf diesem Gerät bereits als %1$s angemeldet." "Dein Homeserver muss aktualisiert werden, um den Matrix Authentication Services und die Erstellung von Konten zu unterstützen." "Fehler beim Erstellen des Permalinks" "%1$s konnte die Karte nicht laden. Bitte versuche es später erneut." "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." + "Der Chat 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." + "%1$s hat keine Berechtigung, auf deinen Standort zuzugreifen. Du kannst den Zugriff in den Einstellungen aktivieren." + "%1$s hat keine Berechtigung, auf deinen Standort zuzugreifen. Erlaube unten den Zugriff." + "%1$s hat nicht die Berechtigung auf dein Mikrofon zuzugreifen. Erlaube 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" + "Diese Chat-Adresse existiert bereits. Bitte bearbeite das Adressfeld des Chats oder ändere den Namen des Chats" "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" @@ -365,39 +411,41 @@ Möchten Sie wirklich fortfahren?" "Unverschlüsselt." "Verschlüsselt von einem unbekannten oder gelöschten Gerät." "Verschlüsselt durch ein Gerät, das nicht von seinem Besitzer verifiziert wurde." - "Verschlüsselt durch einen nicht verifizierten Benutzer." + "Verschlüsselt durch einen nicht verifizierten Nutzer." "🔐️ Begleite mich auf %1$s" "Hey, sprich mit mir auf %1$s: %2$s" "%1$s Android" - "Schüttel heftig zum Melden von Fehlern" + "Heftiges Schütteln um Fehler zu melden" + "Bildschirmfoto" "%1$s: %2$s" "Optionen" "Entferne %1$s" "Einstellungen" "Medienauswahl fehlgeschlagen, bitte versuche es erneut." - "Drücke auf eine Nachricht und wähle “%1$s”, um sie hier einzufügen." + "Halte eine Nachricht gedrückt und wähle “%1$s”, um sie hier einzufügen." "Fixiere wichtige Nachrichten, so dass sie leicht gefunden werden können" "%1$d fixierte Nachricht" "%1$d fixierte Nachrichten" "Fixierte Nachrichten" - "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." + "Du wirst jetzt zu deinem %1$s Konto geleitet, um deine Identität zurückzusetzen. Danach wirst du zur App zurückgebracht." + "Kannst du das nicht bestätigen? Gehe zu deinem Konto, um deine Identität zurückzusetzen." "Verifizierung zurückziehen und senden" - "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" + "Du kannst deine Verifizierung zurückziehen und diese Nachricht trotzdem senden, oder du kannst vorerst abbrechen und es später noch einmal versuchen, nachdem du %1$s erneut verifiziert hast." + "Deine Nachricht wurde nicht gesendet, da die verifizierte Identität von %1$s zurückgesetzt wurde" "Nachricht trotzdem senden" - "%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." + "%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." "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." + "Mindestens eines deiner Geräte ist nicht verifiziert. Du kannst die Nachricht trotzdem senden, oder den Vorgang zunächst abbrechen und es später erneut versuchen, nachdem du alle deine Geräte verifiziert hast." + "Deine Nachricht wurde nicht gesendet, da du eines oder mehrere deiner Geräte nicht verifiziert hast." + "Admins oder Eigentümer bearbeiten" "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." - "Benutzerdetails konnten nicht abgerufen werden" + "Nutzerdetails konnten nicht abgerufen werden" "Nachricht in %1$s" "Erweitern" "Verkleinern" - "Du siehst diesen Raum bereits!" + "Du siehst diesen Chat bereits!" "%1$s von %2$s" "%1$s fixierte Nachrichten" "Nachricht wird geladen…" @@ -409,15 +457,19 @@ Möchten Sie wirklich fortfahren?" "In Google Maps öffnen" "In OpenStreetMap öffnen" "Diesen Standort teilen" + "Von dir erstellte oder beigetretene Spaces." + "%1$s • %2$s" + "%1$s Space" + "Spaces" "Nachricht nicht gesendet, weil sich die verifizierte Identität von %1$s geändert hat." "Die Nachricht wurde nicht gesendet, weil %1$s nicht alle Geräte verifiziert hat." - "Die Nachricht wurde nicht gesendet, weil Sie eines oder mehrere Ihrer Geräte nicht verifiziert haben." + "Die Nachricht wurde nicht gesendet, weil du eines oder mehrere deiner Geräte nicht verifiziert hast." "Standort" "Version: %1$s (%2$s)" "en" "Der Nachrichtenverlauf ist auf diesem Gerät nicht verfügbar" - "Für den Zugriff auf den kompletten Nachrichtenverlauf müssen Sie dieses Gerät verifizieren" - "Sie haben keinen Zugriff auf diese Nachricht" + "Für den Zugriff auf den Nachrichtenverlauf musst du dieses Gerät verifizieren" + "Du hast keinen Zugriff auf diese Nachricht." "Nachricht kann nicht entschlüsselt werden" - "Diese Nachricht wurde entweder blockiert, weil Ihr Gerät nicht verifiziert ist oder weil der Absender Ihre Identität überprüfen muss." + "Diese Nachricht wurde entweder blockiert, weil du dein Gerät nicht verifiziert hast oder weil der Absender deine Identität verifizieren muss." diff --git a/libraries/ui-strings/src/main/res/values-eo/translations.xml b/libraries/ui-strings/src/main/res/values-eo/translations.xml new file mode 100644 index 0000000000..6f796aef09 --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-eo/translations.xml @@ -0,0 +1,26 @@ + + + "Manage linked devices" + "Start fresh" + "Backup password" + "Sent from an unconfirmed device" + "Sender\'s security verification has changed" + "Confirm device" + "Verify user" + "%1$s\'s security verification has changed. %2$s" + "%1$s\'s %2$s security verification has changed. %3$s" + "%1$s\'s security verification has changed." + "%1$s\'s %2$s security verification has changed. %3$s" + "You\'re about to go to your %1$s account to start fresh. Afterwards you\'ll be taken back to the app." + "Can\'t confirm? Go to your account to start fresh." + "Your message was not sent because %1$s\'s security verification has changed" + "%1$s is using one or more unconfirmed devices. You can send the message anyway, or you can cancel for now and try again later after %2$s has confirmed all their devices." + "Your message was not sent because %1$s has not confirmed all devices" + "One or more of your linked devices are unconfirmed. You can send the message anyway, or you can cancel for now and try again later after you have confirmed all of your linked devices." + "Your message was not sent because you have not confirmed one or more of your linked devices" + "Message not sent because %1$s\'s security verification has changed." + "Message not sent because %1$s has not confirmed all their devices." + "Message not sent because you have not confirmed one or more of your linked devices." + "You need to confirm this device for access to historical messages" + "This message was blocked either because you did not confirm your device or because the sender needs to verify you." + 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 a6291130f9..d4c96c52db 100644 --- a/libraries/ui-strings/src/main/res/values-et/translations.xml +++ b/libraries/ui-strings/src/main/res/values-et/translations.xml @@ -2,6 +2,7 @@ "Reageeri: %1$s" "Tunnuspilt" + "Vähenda tekstivälja" "Kustuta" "%1$d number sisestatud" @@ -10,6 +11,7 @@ "Muuda tunnuspilti" "Täisaadress saab olema %1$s" "Krüptimise üksikasjad" + "Laienda tekstivälja" "Peida salasõna" "Liitu kõnega" "Mine lõppu" @@ -40,9 +42,9 @@ "Eemalda reageerimine: %1$s" "Jututoa tunnuspilt" "Saada faile" + "Palun tee see ajapiiranguga toiming, sul on aega üks minut" "Näita salasõna" "Helista" - "Palun tee see ajapiiranguga toiming" "Lõpetatuks märgitud jututuba" "Kasutaja tunnuspilt" "Kasutajamenüü" @@ -88,6 +90,7 @@ "Võta kasutusele" "Lõpeta küsitlus" "Sisesta PIN-kood" + "Lõpeta" "Kas unustasid salasõna?" "Edasta" "Tagasi eelmisesse vaatesse" @@ -102,6 +105,7 @@ "Lahku" "Lahku vestlusest" "Lahku jututoast" + "Lahku kogukonnast" "Näita veel" "Halda kasutajakontot" "Halda seadmeid" @@ -162,10 +166,14 @@ "Saadaval on uuendus" "Rakenduse teave" "Vastuvõetava kasutamise põhimõtted" + "Lisa kasutajakonto" + "Lisa veel üks kasutajakonto" "Lisame selgitust" "Täiendavad seadistused" "pilt" "Analüütika" + "Sa oled jututoast lahkunud" + "Sa olid sessioonist väljaloginud" "Välimus" "Heli" "Blokeeritud kasutajad" @@ -177,9 +185,12 @@ "Loome jututoa…" "Päring on tühistatud" "Lahkus jututoast" + "Lahkus kogukonnast" "Keeldusid kutsest" "Tume" "Dekrüptimisviga" + "Kirjeldus" + "Eemalda kõik valikud" "Arendaja valikud" "Seadme tunnus" "Otsevestlus" @@ -236,6 +247,7 @@ Põhjus: %1$s." "%1$s (%2$s)" "Otsingul pole tulemusi" "Jututoal puudub nimi" + "Kogukonnal pole nime" "Krüptimata" "Võrgust väljas" "Avatud lähtekoodiga litsentsid" @@ -289,18 +301,23 @@ Põhjus: %1$s." "Otsingutulemused" "Turvalisus" "Seda nägi(d)" + "Vali kasutajakonto" + "Vali kõik" "Saada kasutajale" "Saadame…" "Saatmine ei õnnestunud" "Saadetud" ". " "Server pole toetatud" + "Server pole leitav" "Serveri URL" "Seadistused" + "Jaga kogukonda" "Jagatud asukoht" "Logime välja" "Midagi läks valesti" "Tekkis viga. Palun proovi uuesti." + "Kogukond" "%1$d kogukond" "%1$d kogukonda" @@ -370,6 +387,8 @@ Kas sa oled kindel, et soovid jätkata?" "Suurim lubatud failisuurus on: %1$s" "Vali üleslaaditava video kvaliteet." "Vali video kvaliteet." + "Otsi emojisid" + "Sa juba oled sellesse seadmesse sisseloginud kasutajana %1$s." "Selleks et koos kasutajakonto loomisega toimiks Matrix Authentication Service\'i tugi, vajab sinu koduserver uuendamist." "Püsilingi loomine ei õnnestumud" "%1$s kaardi laadimine ei õnnestunud. Palun proovi hiljem uuesti." @@ -429,7 +448,7 @@ Kas sa oled kindel, et soovid jätkata?" "Sa juba vaatad seda jututuba!" "%1$s / %2$s" "%1$s esiletõstetud sõnumit" - "Laadime sõnumit…" + "Laadin sõnumit…" "Näita kõiki" "Vestlus" "Jaga asukohta" @@ -440,6 +459,7 @@ Kas sa oled kindel, et soovid jätkata?" "Jaga seda asukohta" "Sinu loodud kogukonnad ning need, millega oled liitunud." "%1$s • %2$s" + "Kogukond: %1$s" "Kogukonnad" "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." diff --git a/libraries/ui-strings/src/main/res/values-eu/translations.xml b/libraries/ui-strings/src/main/res/values-eu/translations.xml index d726761ccf..28f8abc98e 100644 --- a/libraries/ui-strings/src/main/res/values-eu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-eu/translations.xml @@ -272,6 +272,7 @@ Arrazoia: %1$s." "Bidaltzen…" "Bidalketak huts egin du" "Bidalita" + ". " "Zerbitzaria ez da bateragarria" "Zerbitzariaren URLa" "Ezarpenak" @@ -323,8 +324,13 @@ Arrazoia: %1$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" + "%1$s estekak %2$s gunera zaramatza. + +Ziur jarraitu nahi duzula?" "Egiaztatu honako esteka" "Bideoaren igoera-kalitatea" + "Onartutako fitxategiaren gehienezko tamaina %1$s da" + "Fitxategiaren tamaina handiegia da igotzeko" "Gela salatu da" "Gela salatu eta utzi da" "Baieztapena" @@ -354,6 +360,10 @@ Arrazoia: %1$s." "Astindu erroreen berri emateko" "Aukerak" "Huts egin du multimedia aukeratzeak, saiatu berriro." + + "Finkatutako mezu %1$d" + "Finkatutako %1$d mezu" + "Finkatutako mezuak" "Bidali mezua hala ere" "%2$s(e)tik %1$s" 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 d09b945bad..c599150e83 100644 --- a/libraries/ui-strings/src/main/res/values-fa/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fa/translations.xml @@ -36,9 +36,9 @@ "برداشتن واکنش با %1$s" "چهرک اتاق" "ارسال پرونده‌ها" + "نیازمند کنش محدود به زمان" "نمایش گذرواژه" "آغاز یک تماس" - "نیازمند کنش محدود به زمان" "چهرک کاربر" "فهرست کاربر" "دیدن چهرک" diff --git a/libraries/ui-strings/src/main/res/values-fi/translations.xml b/libraries/ui-strings/src/main/res/values-fi/translations.xml index 6ebf3b84e1..f819a17bfa 100644 --- a/libraries/ui-strings/src/main/res/values-fi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml @@ -2,6 +2,7 @@ "Lisää reaktio: %1$s" "Avatar" + "Pienennä viestin tekstikenttä" "Poista" "%1$d numero syötetty" @@ -10,6 +11,7 @@ "Muokkaa avataria" "Täysi osoite tulee olemaan %1$s" "Salauksen tiedot" + "Laajenna viestin tekstikenttä" "Piilota salasana" "Liity puheluun" "Siirry loppuun" @@ -40,9 +42,9 @@ "Poista reaktio: %1$s" "Huoneen avatar" "Lähetä tiedostoja" + "Aikarajoitettu toimenpide vaaditaan, sinulla on yksi minuutti aikaa vahvistaa" "Näytä salasana" "Aloita puhelu" - "Aikarajoitettu toimenpide vaaditaan" "Haudattu huone" "Käyttäjän avatar" "Käyttäjävalikko" @@ -88,6 +90,7 @@ "Ota käyttöön" "Lopeta kysely" "Syötä PIN-koodi" + "Valmis" "Unohditko salasanan?" "Välitä" "Takaisin" @@ -102,6 +105,7 @@ "Poistu" "Poistu keskustelusta" "Poistu huoneesta" + "Poistu tilasta" "Lataa lisää" "Hallitse tiliä" "Hallitse laitteita" @@ -162,10 +166,14 @@ "Päivitys saatavilla" "Tietoa" "Hyväksyttävän käytön käytäntö" + "Lisää tili" + "Lisää toinen tili" "Lisätään kuvatekstiä" "Edistyneet asetukset" "kuva" "Analytiikka" + "Poistuit huoneesta" + "Sinut kirjattiin ulos istunnosta" "Ulkoasu" "Ääni" "Estetyt käyttäjät" @@ -177,9 +185,11 @@ "Luodaan huonetta…" "Pyyntö peruutettu" "Poistuit huoneesta" + "Poistuit tilasta" "Kutsu hylätty" "Tumma" "Salauksen purkuvirhe" + "Kuvaus" "Kehittäjän asetukset" "Laitteen tunnus" "Yksityinen keskustelu" @@ -289,18 +299,22 @@ Syy: %1$s." "Hakutulokset" "Turvallisuus" "Nähneet henkilöt" + "Valitse tili" "Jaa" "Lähetetään…" "Lähetys epäonnistui" "Lähetetty" ". " "Palvelin ei ole tuettu" + "Palvelimeen ei saada yhteyttä" "Palvelimen osoite" "Asetukset" + "Jaa tila" "Jaettu sijainti" "Kirjaudutaan ulos" "Jokin meni pieleen" "Kohtasimme ongelman. Yritä uudelleen." + "Tila" "%1$d Tila" "%1$d Tilaa" @@ -335,6 +349,12 @@ Syy: %1$s." "Vahvista identiteetti" "Vahvista käyttäjä" "Video" + "Korkea laatu" + "Paras laatu, mutta suurempi tiedostokoko" + "Heikko laatu" + "Nopein lähetysnopeus ja pienin tiedostokoko" + "Normaali laatu" + "Tasapainotettu laatu ja lähetysnopeus" "Ääniviesti" "Odotetaan…" "Odotetaan viestiä" @@ -349,6 +369,10 @@ Syy: %1$s." Haluatko varmasti jatkaa?" "Tarkista tämä linkki" + "Valitse lähettämäsi videoiden oletuslaatu." + "Videoiden lähetyslaatu" + "Suurin sallittu tiedostokoko on: %1$s" + "Tiedostokoko on liian suuri lähetettäväksi" "Huone ilmoitettu" "Ilmoitettu ja poistuttu huoneesta" "Vahvistus" @@ -356,7 +380,12 @@ Haluatko varmasti jatkaa?" "Onnistui" "Varoitus" "Muutoksiasi ei ole tallennettu. Haluatko varmasti palata takaisin?" - "Tallenna muutokset?" + "Tallennetaanko muutokset?" + "Suurin sallittu tiedostokoko on: %1$s" + "Valitse lähetettävän videon laatu." + "Valitse videon lähetyslaatu" + "Etsi emojeja" + "Olet jo kirjautuneena tälle laitteelle käyttäjällä %1$s." "Kotipalvelimesi on päivitettävä tukemaan Matrix Authentication Serviceä ja tilin luomista." "Pysyvän linkin luominen epäonnistui" "%1$s ei pystynyt lataamaan karttaa. Yritä myöhemmin uudelleen." 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 564431362b..9d3555baaa 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -2,6 +2,7 @@ "Ajouter une réaction: %1$s" "Avatar" + "Réduire la taille du composeur" "Supprimer" "%1$d chiffre saisi" @@ -10,6 +11,7 @@ "Modifier l’avatar" "L’adresse complète sera %1$s" "Détails du chiffrement" + "Augmenter la taille du composeur" "Masquer le mot de passe" "Rejoindre l’appel" "Retourner à la fin de la conversation" @@ -40,9 +42,9 @@ "Supprimer la réaction avec %1$s" "Avatar du salon" "Envoyer des fichiers" + "Action limitée dans le temps requise, vous avez une minute pour effectuer la vérification" "Afficher le mot de passe" "Démarrer un appel" - "Demande d’action qui expirera dans un instant" "Salon clôturé" "Avatar de l’utilisateur" "Menu utilisateur" @@ -88,6 +90,7 @@ "Activer" "Terminer le sondage" "Saisir le code PIN" + "Terminer" "Mot de passe oublié ?" "Transférer" "Retour" @@ -102,6 +105,7 @@ "Quitter" "Quitter la discussion" "Quitter le salon" + "Quitter l’espace" "Voir plus" "Gérer le compte" "Gérez les sessions" @@ -162,10 +166,14 @@ "Mise à niveau disponible" "À propos" "Politique d’utilisation acceptable" + "Ajouter un compte" + "Ajouter un autre compte" "Ajout d’une légende" "Paramètres avancés" "une image" "Statistiques d’utilisation" + "Vous avez quitter le salon" + "Vous avez été déconnecté de la session" "Apparence" "Audio" "Utilisateurs bloqués" @@ -176,10 +184,13 @@ "Droits d’auteur" "Création du salon…" "Demande annulée" - "Quitter le salon" + "Vous avez quitté le salon" + "Vous avez quitté l’espace" "Invitation refusée" "Sombre" "Erreur de déchiffrement" + "Description" + "Tout désélectionner" "Options pour les développeurs" "Identifiant de session" "Discussion à deux" @@ -236,6 +247,7 @@ Raison : %1$s." "%1$s (%2$s)" "Aucun résultat" "Salon sans nom" + "Espace sans nom" "Non chiffré" "Hors ligne" "Licences open source" @@ -289,18 +301,23 @@ Raison : %1$s." "Résultats de la recherche" "Sécurité" "Vu par" + "Choisir un compte" + "Tout sélectionner" "Envoyer vers" "Envoi en cours…" "Échec de l’envoi" "Envoyé" ". " "Serveur non pris en charge" + "Serveur inaccessible" "URL du serveur" "Paramètres" + "Partager l’espace" "Position partagée" "Déconnexion" "Une erreur s’est produite" "Nous avons rencontré un problème. Veuillez réessayer." + "Espace" "%1$d Espace" "%1$d Espaces" @@ -370,6 +387,8 @@ Raison : %1$s." "La taille maximale de fichier autorisée est: %1$s" "Sélectionnez la qualité des vidéos que vous souhaitez envoyer." "Sélectionnez la qualité d’envoi des vidéos" + "Chercher des emojis" + "Vous êtes déjà connecté sur cet appareil en tant que %1$s." "Votre serveur d’accueil doit être mis à jour pour prendre en charge le protocole MAS (Matrix Authentication Service) et la création de compte." "Échec de la création du permalien" "%1$s n’a pas pu charger la carte. Veuillez réessayer ultérieurement." @@ -440,6 +459,7 @@ Raison : %1$s." "Partager cette position" "Espaces que vous avez créés ou rejoints." "%1$s • %2$s" + "Espace %1$s" "Espaces" "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." 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 62b0373451..a31d12be62 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -2,6 +2,7 @@ "Reakció hozzáadása: %1$s" "Profilkép" + "Üzenet szövegmezőjének minimalizálása" "Törlés" "%1$d megadott számjegy" @@ -10,6 +11,7 @@ "Profilkép szerkesztése" "A teljes cím ez lesz: %1$s" "Titkosítás részletei" + "Üzenet szövegmezőjének kibontása" "Jelszó elrejtése" "Csatlakozás a híváshoz" "Ugrás az aljára" @@ -40,9 +42,9 @@ "Reakció eltávolítása: %1$s" "Szoba profilképe" "Fájlküldés" + "Időkorlátos művelet szükséges, egy perce van az ellenőrzésre" "Jelszó megjelenítése" "Hanghívás indítása" - "Időkorlátos művelet szükséges" "Elévült szoba" "Felhasználói profilkép" "Felhasználói menü" @@ -69,7 +71,7 @@ "Másolás" "Felirat másolása" "Hivatkozás másolása" - "Üzenetre mutató hivatkozás másolása" + "Üzenethivatkozás másolása" "Szöveg másolása" "Létrehozás" "Szoba létrehozása" @@ -103,6 +105,7 @@ "Elhagyás" "Beszélgetés elhagyása" "Szoba elhagyása" + "Tér elhagyása" "Továbbiak betöltése" "Fiók kezelése" "Eszközök kezelése" @@ -163,10 +166,14 @@ "Frissítés érhető el" "Névjegy" "Elfogadható használatra vonatkozó szabályzat" + "Fiók hozzáadása" + "Másik fiók hozzáadása" "Felirat hozzáadása" "Speciális beállítások" "egy kép" "Elemzések" + "Elhagyta a szobát" + "Ki lett jelentkeztetve a munkamenetből" "Megjelenítés" "Hang" "Letiltott felhasználók" @@ -178,9 +185,12 @@ "Szoba létrehozása…" "Kérés megszakítva" "Elhagyta a szobát" + "Tér elhagyva" "Meghívás elutasítva" "Sötét" "Visszafejtési hiba" + "Leírás" + "Kijelölés megszüntetése" "Fejlesztői beállítások" "Eszközazonosító" "Közvetlen csevegés" @@ -237,6 +247,7 @@ Ok: %1$s." "%1$s (%2$s)" "Nincs találat" "Nincs szobanév" + "Nincs térnév" "Nincs titkosítva" "Kapcsolat nélkül" "Nyílt forráskódú licencek" @@ -289,14 +300,18 @@ Ok: %1$s." "Keresési találatok" "Biztonság" "Látta" + "Fiók kiválasztása" + "Összes kijelölése" "Címzett" "Küldés…" "A küldés sikertelen" "Elküldve" ". " "A kiszolgáló nem támogatott" + "A kiszolgáló nem érhető el" "Kiszolgáló webcíme" "Beállítások" + "Tér megosztása" "Megosztott tartózkodási hely" "Kijelentkezés" "Valamilyen hiba történt" @@ -371,6 +386,8 @@ Biztos, hogy folytatja?" "A legnagyobb megengedett fájlméret: %1$s" "Válassza ki a feltöltendő videó minőségét." "Feltöltött videó minőségének kiválasztása" + "Emodzsik keresése" + "Már bejelentkezett erre az eszközre, mint %1$s." "A Matrix-kiszolgálót frissíteni kell a Matrix Authentication Service és a fióklétrehozás támogatásához." "Nem sikerült létrehozni az állandó hivatkozást" "Az %1$s nem tudta betölteni a térképet. Próbálja meg újra később." @@ -428,7 +445,7 @@ Biztos, hogy folytatja?" "Kibontás" "Csökkentés" "Már ezt a szobát nézi!" - "%1$s / %2$s" + "%1$s. / %2$s" "%1$s kitűzött üzenet" "Üzenet betöltése…" "Összes megtekintése" @@ -441,6 +458,7 @@ Biztos, hogy folytatja?" "E hely megosztása" "Létrehozott vagy olyan terek, melyekhez csatlakozott." "%1$s • %2$s" + "%1$s tér" "Terek" "Az üzenet nem lett elküldve, mert %1$s ellenőrzött személyazonossága megváltozott." "Az üzenet nem lett elküldve, mert %1$s nem ellenőrizte az összes eszközét." 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 294ab5fd27..bd7739d5ba 100644 --- a/libraries/ui-strings/src/main/res/values-in/translations.xml +++ b/libraries/ui-strings/src/main/res/values-in/translations.xml @@ -11,6 +11,9 @@ "Lompat ke bawah" "Hanya sebutan" "Dibisukan" + "Sebutan baru" + "Pesan baru" + "Panggilan berlangsung" "Avatar pengguna lain" "Halaman %1$d" "Jeda" @@ -99,6 +102,7 @@ "Tidak" "Jangan sekarang" "Oke" + "Buka menu konteks" "Pengaturan" "Buka dengan" "Sematkan" @@ -106,9 +110,9 @@ "Kutip" "Bereaksi" "Tolak" - "Hapus" - "Hapus keterangan" - "Hapus pesan" + "Hilangkan" + "Hilangkan keterangan" + "Hilangkan pesan" "Balas" "Balas dalam utas" "Laporkan" @@ -123,7 +127,9 @@ "Simpan" "Cari" "Kirim" + "Kirim pesan tersunting" "Kirim pesan" + "Kirim pesan suara" "Bagikan" "Bagikan tautan" "Tampilkan" @@ -209,7 +215,7 @@ Alasan: %1$s." "%d lainnya" - "%1$d anggota" + "%1$d Anggota" "Pesan" "Tindakan pesan" 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 c5bc5e45b2..77a1a20782 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -2,16 +2,26 @@ "Aggiungi reazione: %1$s" "Avatar" + "Riduci al minimo il campo di testo del messaggio" "Elimina" "%1$d cifra inserita" "%1$d cifre inserite" + "Modifica avatar" + "L\'indirizzo completo sarà %1$s" + "Dettagli sulla crittografia" + "Allarga il campo di testo del messaggio" "Nascondi password" "Entra in chiamata" "Vai alla fine" + "Sposta la mappa sulla mia posizione" "Solo menzioni" "Silenziato" + "Nuove menzioni" + "Nuovi messaggi" + "Chiamata in corso" + "Avatar dell\'altro utente" "Pagina %1$d" "Pausa" "Messaggio vocale, durata: %1$s, posizione attuale: %2$s" @@ -30,15 +40,20 @@ "Tocca per mostrare tutti" "Rimuovi la reazione con %1$s" "Rimuovere la reazione con %1$s" + "Avatar della stanza" "Invia file" + "Azione a tempo limitato richiesta" "Mostra password" "Avvia una chiamata" + "Stanza obsoleta" + "Avatar utente" "Menu utente" "Visualizza avatar" "Visualizza dettagli" "Messaggio vocale, durata: %1$s" "Registra un messaggio vocale." "Ferma la registrazione" + "Il tuo avatar" "Accetta" "Aggiungi didascalia" "Aggiungi alla conversazione" @@ -75,6 +90,7 @@ "Attiva" "Termina sondaggio" "Inserisci PIN" + "Fine" "Password dimenticata?" "Inoltra" "Indietro" @@ -97,6 +113,7 @@ "No" "Non ora" "OK" + "Apri il menu contestuale" "Impostazioni" "Apri con" "Fissa" @@ -121,7 +138,9 @@ "Salva" "Ricerca" "Invia" + "Invia messaggio modificato" "Invia messaggio" + "Invia messaggio vocale" "Condividi" "Condividi collegamento" "Mostra" @@ -137,6 +156,7 @@ "Tocca per le opzioni" "Riprova" "Rimuovi dai fissati" + "Visualizza" "Visualizza nella conversazione" "Vedi codice sorgente" "Sì" @@ -149,6 +169,8 @@ "Impostazioni avanzate" "un\'immagine" "Statistiche di utilizzo" + "Hai lasciato la stanza" + "Sei stato disconnesso dalla sessione" "Aspetto" "Audio" "Utenti bloccati" @@ -219,7 +241,7 @@ Motivo:. %1$s" "%1$s (%2$s)" "Nessun risultato" "Nessun nome della stanza" - "Non cifrata" + "Non cifrato" "Non in linea" "Licenze open source" "o" @@ -238,9 +260,12 @@ Motivo:. %1$s" "%d voto" "%d voti" + "Preparazione…" "Informativa sulla privacy" "Stanza privata" + "Spazio privato" "Stanza pubblica" + "Spazio pubblico" "Reazione" "Reazioni" "Motivo" @@ -258,6 +283,10 @@ Motivo:. %1$s" "Stanza" "Nome stanza" "ad es. il nome del tuo progetto" + + "%1$d Stanza" + "%1$d Stanze" + "Modifiche salvate" "Salvataggio" "Blocco schermo" @@ -277,6 +306,11 @@ Motivo:. %1$s" "Disconnessione" "Qualcosa è andato storto" "Abbiamo riscontrato un problema. Per favore riprova." + "Spazio" + + "%1$d Spazio" + "%1$d Spazi" + "Avvio della conversazione…" "Adesivo" "Operazione riuscita" @@ -307,6 +341,12 @@ Motivo:. %1$s" "Verifica l\'identità" "Verifica utente" "Video" + "Qualità alta" + "Migliore qualità ma dimensioni del file maggiori" + "Qualità bassa" + "Velocità di caricamento più elevata e dimensioni file più piccole" + "Qualità standard" + "Equilibrio tra qualità e velocità di caricamento" "Messaggio vocale" "In attesa…" "In attesa del messaggio" @@ -321,6 +361,10 @@ Motivo:. %1$s" Sei sicuro di voler continuare?" "Ricontrolla questo link" + "Seleziona la qualità predefinita dei video che carichi." + "Qualità del caricamento video" + "La dimensione massima consentita per il file è: %1$s" + "La dimensione del file è troppo grande per essere caricata" "Stanza segnalata" "Stanza segnalata ed abbandonata" "Conferma" @@ -329,6 +373,9 @@ Sei sicuro di voler continuare?" "Attenzione" "Le modifiche non sono state salvate. Vuoi davvero tornare indietro?" "Salvare le modifiche?" + "La dimensione massima consentita per il file è: %1$s" + "Seleziona la qualità del video che vuoi caricare." + "Seleziona la qualità di caricamento del video" "Il tuo homeserver deve essere aggiornato per supportare il Matrix Authentication Service e la creazione di account." "Impossibile creare il collegamento permanente" "%1$s non è riuscito a caricare la mappa. Riprova più tardi." @@ -356,6 +403,7 @@ Sei sicuro di voler continuare?" "Ehi, parliamo su %1$s: %2$s" "%1$s Android" "Scuoti per segnalare un problema" + "Istantanea schermo" "%1$s: %2$s" "Risposte" "Rimuovi %1$s" @@ -378,6 +426,7 @@ Sei sicuro di voler continuare?" "Il tuo messaggio non è stato inviato perché %1$s non ha verificato tutti i dispositivi." "Uno o più dispositivi non sono verificati. Puoi inviare il messaggio comunque, oppure annullarlo e riprovare più tardi dopo aver verificato tutti i tuoi dispositivi." "Il tuo messaggio non è stato inviato perché non hai verificato uno o più dispositivi." + "Modifica amministratori o proprietari" "Elaborazione del file multimediale da caricare fallita, riprova." "Impossibile recuperare i dettagli dell\'utente" "Messaggio in %1$s" @@ -395,6 +444,9 @@ Sei sicuro di voler continuare?" "Apri in Google Maps" "Apri in OpenStreetMap" "Condividi questa posizione" + "Spazi che hai creato o a cui hai aderito." + "%1$s • %2$s" + "Spazi" "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." diff --git a/libraries/ui-strings/src/main/res/values-ko/translations.xml b/libraries/ui-strings/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..5d9dcbe2f2 --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-ko/translations.xml @@ -0,0 +1,454 @@ + + + "반응 추가: %1$s" + "아바타" + "메시지 텍스트 필드 최소화" + "삭제" + + "%1$d자리 입력됨" + + "아바타 편집" + "전체 주소는 다음과 같습니다 %1$s" + "암호화 세부 정보" + "메시지 텍스트 필드 확장" + "비밀번호 숨기기" + "통화 참가" + "맨 아래로 이동" + "지도를 내 위치로 이동" + "멘션만" + "음소거함" + "새로운 언급" + "새 메시지" + "진행 중인 통화" + "다른 사용자의 아바타" + "페이지 %1$d" + "일시중지" + "음성 메시지, 지속 시간: %1$s, 현재 위치: %2$s" + "PIN 필드" + "재생" + "투표" + "종료된 투표" + "%1$s에 반응하세요" + "다른 이모지로 반응하세요" + "읽은 사람 %1$s 그리고 %2$s" + + "읽은 사람 %1$s 그리고 %2$d 다른 사람들" + + "%1$s 님이 읽음" + "모두 표시하려면 탭하세요" + "반응 제거: %1$s" + "%1$s 반응을 제거하세요" + "방 아바타" + "파일 보내기" + "시간 제한 조치가 필요합니다" + "비밀번호 표시" + "통화 시작" + "묘비 방" + "사용자 아바타" + "사용자 메뉴" + "아바타 보기" + "세부 정보 보기" + "음성 메시지, 재생 시간: %1$s" + "음성 메시지를 녹음합니다." + "녹화 중지" + "당신의 아바타" + "수락" + "캡션 추가" + "타임라인에 추가" + "이전" + "통화" + "취소" + "현재 취소" + "사진 선택" + "지우기" + "닫기" + "인증 완료" + "확인" + "비밀번호 확인" + "계속" + "복사" + "캡션 복사" + "링크 복사" + "메시지에 링크 복사" + "텍스트 복사" + "만들기" + "방 만들기" + "비활성화" + "계정 비활성화" + "거절" + "거부 및 차단" + "투표 삭제" + "비활성화" + "취소" + "닫기" + "완료" + "편집" + "캡션 편집" + "투표 수정" + "활성화" + "투표 종료" + "PIN을 입력하세요" + "완료" + "비밀번호를 잊으셨나요?" + "전달" + "뒤로 가기" + "무시하다" + "초대" + "사람 초대하기" + "%1$s에 친구 초대" + "%1$s에 사람 초대" + "초대" + "참가하기" + "더 알아보기" + "떠나기" + "대화에서 나가기" + "방 떠나기" + "더 불러오기" + "계정 관리" + "기기 관리" + "메시지" + "다음" + "아니오" + "나중에" + "확인" + "컨텍스트 메뉴 열기" + "다음" + "다음으로 열기" + "고정" + "빠른 답장" + "인용" + "반응" + "거부" + "제거" + "캡션 제거" + "메시지 삭제" + "답변" + "스레드에서 답장" + "신고" + "버그 보고" + "컨텐츠 신고" + "대화 신고" + "방 신고" + "초기화" + "신원 재설정" + "재시도" + "복호화 재시도" + "저장" + "검색" + "보내기" + "편집한 메시지 보내기" + "메시지 보내기" + "음성 메세지 보내기" + "공유" + "링크 공유" + "표시" + "다시 로그인" + "로그아웃" + "무시하고 로그아웃" + "건너뛰기" + "시작" + "채팅 시작" + "인증 시작" + "탭해서 지도 불러오기" + "사진 찍기" + "옵션을 보려면 탭하세요" + "다시 시도하기" + "고정 해제" + "보기" + "타임라인에서 보기" + "소스 보기" + "예" + "네, 다시 시도하세요" + "이제 서버가 새롭고 더 빠른 프로토콜을 지원합니다. 지금 로그아웃한 다음 다시 로그인하여 업그레이드하세요. 지금 업그레이드하면 나중에 이전 프로토콜이 제거될 때 강제 로그아웃되는 것을 방지할 수 있습니다." + "업그레이드 가능" + "정보" + "이용 목적 제한 방침" + "캡션 추가" + "고급 설정" + "이미지" + "통계" + "방에서 나갔습니다" + "세션에서 로그아웃되었습니다." + "외관" + "소리" + "차단한 사용자" + "버블" + "통화 시작" + "채팅 백업" + "클립보드에 복사됨" + "저작권" + "방 만드는 중…" + "요청이 취소되었습니다" + "방 떠남" + "초대 거부됨" + "다크" + "복호화 오류" + "개발자 설정" + "기기 ID" + "다이렉트 채팅" + "이 메시지를 다시 표시하지 마세요" + "다운로드 실패" + "다운로드 중" + "(수정됨)" + "수정 중" + "캡션 편집" + "* %1$s %2$s" + "빈 파일" + "암호화" + "암호화 활성화됨" + "PIN을 입력하세요" + "오류" + "오류가 발생했습니다, 새 메시지 알림을 받지 못할 수 있습니다. 설정에서 알림 문제를 해결하세요. + +이유: %1$s." + "모두" + "실패" + "즐겨찾기" + "즐겨찾기 됨" + "파일" + "파일 삭제됨" + "파일 저장됨" + "파일이 다운로드에 저장됨" + "메시지 전달" + "자주 사용되는" + "GIF" + "이미지" + "%1$s에게 답장" + "APK 설치" + "Matrix ID를 찾을 수 없기 때문에 초대가 수신되지 않을 수도 있습니다." + "방을 떠나는 중" + "라이트" + "줄이 클립보드에 복사되었습니다." + "링크가 클립보드에 복사됨" + "로딩 중…" + "더 많은 내용이 로딩 중…" + + "%d 기타" + + + "%1$d 회원" + + "메시지" + "메시지 작업" + "메시지 레이아웃" + "메시지 제거됨" + "모던" + "음소거" + "%1$s (%2$s)" + "결과 없음" + "방 이름 없음" + "암호화되지 않음" + "오프라인" + "오픈 소스 라이선스" + "또는" + "비밀번호" + "사람" + "퍼머링크" + "권한" + "고정됨" + "인터넷 연결을 확인해 주세요" + "기다려 주세요…" + "정말로 이 투표를 종료하시겠어요?" + "투표: %1$s" + "총 투표수: %1$s" + "결과는 투표가 끝난 이후에 공개됨" + + "%d 에 투표" + + "준비 중…" + "개인정보 처리방침" + "비공개 방" + "비공개 스페이스" + "공개 방" + "공개 스페이스" + "반응" + "반응" + "이유" + "복구 키" + "새로고침 중…" + + "%1$d 답변" + + "%1$s님에게 답장하는 중" + "버그 보고" + "문제 보고" + "보고 제출됨" + "리치 텍스트 편집기" + "방" + "방 이름" + "예: 프로젝트명" + + "%1$d 방" + + "저장된 변경 사항" + "저장" + "화면 잠금" + "사람 검색하기" + "검색 결과" + "보안" + "본 사람" + "보내기" + "전송 중…" + "전송 실패" + "보냄" + ". " + "지원되지 않는 서버" + "서버 URL" + "설정" + "공유된 위치" + "로그아웃" + "뭔가 잘못됐어요" + "문제가 발생했습니다. 다시 시도해 주세요." + "스페이스" + + "%1$d 스페이스" + + "채팅 시작 중…" + "스티커" + "성공" + "제안" + "동기화 중" + "시스템" + "글자" + "제3자 고지" + "스레드" + "주제" + "여기는 무슨 방인가요?" + "해독 불가" + "보안되지 않은 장치에서 전송됨" + "이 메시지에 액세스할 수 없습니다" + "발신자의 검증된 신원이 재설정되었습니다." + "한 명 이상의 사용자에게 초대를 보낼 수 없습니다." + "초대를 보낼 수 없음" + "잠금 해제" + "음소거 해제" + "지원되지 않는 통화" + "지원되지 않는 이벤트" + "아이디" + "인증 취소됨" + "인증 완료" + "검증 실패" + "검증됨" + "기기 인증" + "신원 확인" + "사용자 검증" + "동영상" + "고품질" + "최고의 품질이지만 파일 크기가 더 큽니다." + "저품질" + "가장 빠른 업로드 속도와 가장 작은 파일 크기" + "표준 품질" + "품질과 업로드 속도의 균형" + "음성 메시지" + "대기 중…" + "이 메시지를 기다리고 있습니다" + "당신" + "%1$s 의 신원이 재설정되었습니다. %2$s" + "%1$s의 %2$s 신원이 재설정되었습니다. %3$s" + "(%1$s)" + "%1$s의 신원이 재설정되었습니다." + "%1$s의 %2$s 신원이 재설정되었습니다. %3$s" + "확인 취소" + "%1$s 링크는 다른 사이트로 이동합니다 %2$s + +정말 계속 진행하시겠습니까?" + "이 링크를 다시 확인하세요." + "업로드하는 비디오의 기본 품질을 선택하세요." + "비디오 업로드 품질" + "허용되는 최대 파일 크기: %1$s +" + "파일 크기가 너무 커서 업로드할 수 없습니다." + "방 신고됨" + "신고 후 방 나가기" + "확인" + "오류" + "성공" + "경고" + "변경 내용이 저장되지 않았습니다. 정말로 돌아가시겠습니까?" + "변경 사항을 저장하시겠습니까?" + "허용되는 최대 파일 크기: %1$s +" + "업로드할 비디오의 품질을 선택하세요." + "비디오 업로드 품질 선택" + "Matrix Authentication Service 및 계정 생성을 지원하려면 홈서버를 업그레이드해야 합니다." + "퍼머링크 생성 실패" + "%1$s에서 맵을 로딩할 수 없습니다. 다시 시도해주세요." + "메시지 로딩 실패" + "%1$s가 위치에 접근할 수 없습니다. 나중에 다시 시도해 주세요." + "음성 메시지 업로드에 실패했습니다." + "해당 방이 더 이상 존재하지 않거나 초대장이 더 이상 유효하지 않습니다." + "메시지를 찾을 수 없습니다" + "%1$s에서 위치에 접근할 수 있는 권한이 없습니다. 설정에서 활성화가 가능합니다." + "%1$s에서 위치에 접근할 수 있는 권한이 없습니다. 아래에서 허용해주세요." + "%1$s 는 마이크에 액세스할 수 있는 권한이 없습니다. 음성 메시지를 녹음할 수 있도록 액세스를 허용하세요." + "이것은 네트워크 또는 서버 문제로 인해 발생할 수 있습니다." + "이 방 주소는 이미 존재합니다. 방 주소 필드를 편집하거나 방 이름을 변경해 보세요." + "일부 문자는 허용되지 않습니다. 로마자, 숫자 및 다음 기호만 지원됩니다! $ &amp; ' ( ) * + / ; = ? @ [ ] - . _" + "일부 메시지가 전송되지 않았습니다" + "이런, 오류가 발생했어요" + "이벤트의 발신자가 이벤트를 보낸 장치의 소유자와 일치하지 않습니다." + "이 장치에서는 이 암호화된 메시지의 진위 여부를 보장할 수 없습니다." + "이전에 검증된 사용자에 의해 암호화되었습니다." + "암호화되지 않음." + "알 수 없거나 삭제된 장치에 의해 암호화됩니다." + "소유자가 확인하지 않은 장치에 의해 암호화되었습니다." + "검증되지 않은 사용자에 의해 암호화되었습니다." + "🔐️ %1$s에 참여하기" + "%1$s에서 대화해요: %2$s" + "%1$s Android" + "강하게 흔들어서 오류 보고하기" + "스크린샷" + "%1$s: %2$s" + "옵션" + "%1$s 제거" + "설정" + "미디어 선택에 실패했습니다. 다시 시도해 주세요." + "메시지를 누르고 \"%1$s\" 를 선택하여 여기에 포함합니다." + "중요한 메시지를 고정하여 쉽게 찾을 수 있도록 합니다" + + "%1$d 고정된 메시지" + + "고정된 메세지" + "%1$s 계정으로 이동하여 신원을 재설정하시게 됩니다. 이후 앱으로 돌아가게 됩니다." + "확인할 수 없으신가요? 계정으로 이동하여 신원을 재설정하세요." + "인증 철회 및 전송" + "확인 절차를 철회하고 이 메시지를 보내거나, 지금 취소하고 나중에 %1$s 을 확인한 후 다시 시도할 수 있습니다." + "%1$s의 인증된 신원이 재설정되어 귀하의 메시지가 전송되지 않았습니다." + "아무튼 메시지 보내기" + "%1$s 는 하나 이상의 확인되지 않은 장치를 사용하고 있습니다. 메시지를 보내거나, %2$s 이 모든 장치를 확인한 후에 다시 시도할 수 있습니다." + "%1$s 이(가) 모든 기기를 확인하지 않았기 때문에 귀하의 메시지가 전송되지 않았습니다." + "하나 이상의 기기가 확인되지 않았습니다. 메시지를 보내거나, 모든 기기를 확인한 후 나중에 다시 시도할 수 있습니다." + "하나 이상의 기기를 확인하지 않았기 때문에 메시지가 전송되지 않았습니다" + "관리자 또는 소유자 편집" + "미디어 업로드 처리가 실패했습니다. 다시 시도해 주세요." + "사용자 세부 정보를 가져올 수 없습니다." + "메시지 %1$s" + "펼치기" + "줄이다" + "이 방을 이미 보고 있습니다!" + "%2$s 의 %1$s" + "%1$s 고정된 메시지" + "메시지 로딩 중…" + "모두 보기" + "채팅" + "위치 공유" + "내 위치 공유" + "Apple Maps에서 열기" + "Google Maps에서 열기" + "OpenStreetMap에서 열기" + "이 위치 공유" + "당신이 스페이스를 만들거나 가입했습니다." + "%1$s•%2$s" + "스페이스" + "%1$s의 인증된 신원이 재설정되어 메시지가 전송되지 않았습니다." + "%1$s 이 모든 장치를 확인하지 않았기 때문에 메시지가 전송되지 않았습니다." + "하나 이상의 기기를 확인하지 않았기 때문에 메시지가 전송되지 않았습니다." + "위치" + "버전: %1$s (%2$s)" + "ko" + "이 장치에서는 과거 메시지를 사용할 수 없습니다." + "이전 메시지에 액세스하려면 이 장치를 확인해야 합니다." + "이 메시지에 액세스할 수 없습니다." + "메시지를 해독할 수 없습니다." + "이 메시지는 귀하가 기기를 확인하지 않았거나 발신자가 귀하의 신원을 확인해야 하기 때문에 차단되었습니다." + 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 84bfe86610..24594c782e 100644 --- a/libraries/ui-strings/src/main/res/values-nb/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nb/translations.xml @@ -8,10 +8,12 @@ "%1$d sifre angitt" "Rediger avatar" + "Den fullstendige adressen vil være %1$s" "Krypteringsdetaljer" "Skjul passord" "Bli med i samtale" "Hopp til bunnen" + "Flytt kartet til min lokasjon" "Bare omtaler" "Dempet" "Nye omtaler" @@ -38,9 +40,9 @@ "Fjern reaksjonen med %1$s" "Romavatar" "Sende filer" + "Tidsbegrenset handling kreves, du har ett minutt på deg til å verifisere" "Vis passord" "Start en samtale" - "Tidsbegrenset handling kreves" "Brukeravatar" "Brukermeny" "Vis avatar" @@ -252,9 +254,12 @@ "%d stemme" "%d stemmer" + "Forbereder…" "Retningslinjer for personvern" "Privat rom" + "Privat område" "Offentlig rom" + "Offentlig område" "Reaksjon" "Reaksjoner" "Årsak" @@ -271,6 +276,10 @@ "Rom" "Romnavn" "f.eks. prosjektnavnet ditt" + + "%1$d Rom" + "%1$d Rom" + "Lagrede endringer" "Lagrer" "Skjermlås" @@ -289,6 +298,10 @@ "Logger av" "Noe gikk galt" "Vi har støtt på et problem. Vennligst prøv igjen." + + "%1$d Område" + "%1$d Områder" + "Starter chat…" "Klistremerke" "Suksess" @@ -368,6 +381,7 @@ Er du sikker på at du vil fortsette?" "Hei, snakk med meg på %1$s: %2$s" "%1$s Android" "Rageshake for å rapportere feil" + "Skjermbilde" "%1$s: %2$s" "Alternativer" "Fjern %1$s" @@ -407,6 +421,9 @@ Er du sikker på at du vil fortsette?" "Åpne i Google Maps" "Åpne i OpenStreetMap" "Del denne lokasjonen" + "Områder du har opprettet eller blitt med i." + "%1$s • %2$s" + "Områder" "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." 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 4248212aff..ebbad9ef86 100644 --- a/libraries/ui-strings/src/main/res/values-pl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pl/translations.xml @@ -42,9 +42,9 @@ "Usuń reakcję z %1$s" "Awatar pokoju" "Wyślij pliki" + "Wymagane jest działanie ograniczone czasowo" "Pokaż hasło" "Rozpocznij rozmowę" - "Wymagane jest działanie ograniczone czasowo" "Pokój nagrobkowy" "Awatar użytkownika" "Menu użytkownika" 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 57ecd3388d..fabc600b67 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 @@ -2,17 +2,25 @@ "Adicionar reação: %1$s" "Avatar" - "Excluir" + "Apagar" - "%1$d dígito inserido" - "%1$d dígitos inseridos" + "%1$d dígito digitado" + "%1$d dígitos digitados" + "Editar avatar" + "O endereço completo será %1$s" + "Detalhes de criptografia" "Ocultar senha" - "Juntar-se à chamada" + "Entrar à chamada" "Ir para o final" + "Mover o mapa para a minha localização" "Apenas menções" "Silenciado" - "Página %1$d" + "Novas menções" + "Novas mensagens" + "Chamada em andamento" + "Avatar de outro usuário" + "%1$dª página" "Pausar" "Mensagem de voz, duração: %1$s, posição atual: %2$s" "Campo de PIN" @@ -24,32 +32,37 @@ "Lido por %1$s e %2$s" "Lido por %1$s e %2$d outro" - "Lido por %1$s e %2$d outros" + "Lido por %1$s e outros %2$d" "Lido por %1$s" "Toque para mostrar tudo" "Remover reação com %1$s" "Remover reação com %1$s" + "Avatar da sala" "Enviar arquivos" + "Ação de tempo limitado necessária" "Mostrar senha" "Iniciar uma chamada" + "Sala morta" + "Avatar do usuário" "Menu do usuário" "Ver avatar" "Visualizar detalhes" "Mensagem de voz, duração: %1$s" "Gravar mensagem de voz." "Parar gravação" + "Seu avatar" "Aceitar" "Adicionar legenda" "Adicionar à linha do tempo" "Voltar" "Chamar" "Cancelar" - "Cancelar por agora" + "Cancelar por enquanto" "Escolher foto" "Limpar" "Fechar" - "Verificação completa" + "Concluir a verificação" "Confirmar" "Confirmar senha" "Continuar" @@ -64,17 +77,17 @@ "Desativar conta" "Recusar" "Recusar e bloquear" - "Excluir Enquete" - "Desabilitar" + "Excluir enquete" + "Desativar" "Descartar" "Dispensar" - "Concluído" + "Pronto" "Editar" "Editar legenda" "Editar enquete" - "Habilitar" + "Ativar" "Encerrar enquete" - "Inserir PIN" + "Digitar PIN" "Esqueceu a senha?" "Encaminhar" "Voltar" @@ -93,15 +106,16 @@ "Gerenciar conta" "Gerenciar dispositivos" "Mensagem" - "Próximo" + "Avançar" "Não" "Agora não" "OK" + "Abrir menu de contexto" "Configurações" "Abrir com" "Fixar" "Resposta rápida" - "Citação" + "Citar" "Reagir" "Recusar" "Remover" @@ -109,11 +123,11 @@ "Remover mensagem" "Responder" "Responder no tópico" - "Reportar" - "Reportar erro" - "Reportar conteúdo" - "Reportar conversa" - "Reportar sala" + "Denunciar" + "Reportar bug" + "Denunciar conteúdo" + "Denunciar conversa" + "Denunciar sala" "Redefinir" "Redefinir identidade" "Tentar novamente" @@ -121,15 +135,17 @@ "Salvar" "Pesquisar" "Enviar" + "Enviar mensagem editada" "Enviar mensagem" + "Enviar mensagem de voz" "Compartilhar" "Compartilhar link" "Mostrar" - "Iniciar sessão novamente" + "Entrar novamente" "Sair" "Sair mesmo assim" "Pular" - "Começar" + "Iniciar" "Iniciar conversa" "Iniciar verificação" "Toque para carregar o mapa" @@ -137,11 +153,12 @@ "Toque para opções" "Tente novamente" "Desafixar" + "Visualizar" "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." + "Seu servidor agora é compatível com um protocolo novo e mais rápido. Saia da sua conta e entre novamente para fazer a atualização. Fazendo isso agora, você evitará uma saída forçada quando o protocolo antigo for removido." "Atualização disponível" "Sobre" "Política de uso aceitável" @@ -152,7 +169,7 @@ "Aparência" "Áudio" "Usuários bloqueados" - "Bolhas" + "Balões" "Chamada iniciada" "Backup de conversas" "Copiado para a área de transferência" @@ -163,9 +180,9 @@ "Convite recusado" "Escuro" "Erro de descriptografia" - "Opções do desenvolvedor" + "Opções de desenvolvedor" "ID do dispositivo" - "Conversa privada" + "Conversa direta" "Não mostrar isto novamente" "O download falhou" "Baixando" @@ -176,7 +193,7 @@ "Arquivo vazio" "Criptografia" "Criptografia ativada" - "Insira seu PIN" + "Digite o 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.↵ ↵ @@ -188,9 +205,9 @@ Motivo:​ %1$s." "Arquivo" "Arquivo excluído" "Arquivo salvo" - "Arquivo salvo em Downloads" + "Arquivo salvo nos Downloads" "Encaminhar mensagem" - "Frequentemente usado" + "Usado frequentemente" "GIF" "Imagem" "Em resposta a %1$s" @@ -215,20 +232,20 @@ Motivo:​ %1$s." "Layout da mensagem" "Mensagem removida" "Moderno" - "Silenciar" + "Mudo" "%1$s (%2$s)" - "Sem resultados" - "Sem nome de sala" - "Não criptografado" - "Offline" + "Não há resultados" + "Não há um nome para a sala" + "Sem criptografia" + "Off-line" "Licenças de código aberto" "ou" "Senha" "Pessoas" "Link permanente" "Permissão" - "Fixo" - "Verifique sua conexão com a Internet" + "Fixado" + "Verifique a sua conexão à internet" "Por favor, aguarde…" "Tem certeza de que deseja encerrar esta enquete?" "Enquete: %1$s" @@ -238,31 +255,38 @@ Motivo:​ %1$s." "%d voto" "%d votos" - "Política de Privacidade" + "Preparando…" + "Política de privacidade" "Sala privada" + "Espaço privado" "Sala pública" + "Espaço público" "Reação" "Reações" "Motivo" "Chave de recuperação" - "Atualizando…" + "Recarregando…" "%1$d resposta" "%1$d respostas" "Respondendo a %1$s" - "Reportar um erro" - "Reportar um problema" + "Denunciar um bug" + "Relatar um problema" "Relatório enviado" "Editor de rich text" "Sala" "Nome da sala" "por exemplo, o nome do seu projeto" + + "%1$d sala" + "%1$d salas" + "Alterações salvas" "Salvando" "Bloqueio de tela" - "Procurar alguém" - "Resultados da busca" + "Procurar por alguém" + "Resultados da pesquisa" "Segurança" "Visto por" "Enviar para" @@ -271,32 +295,36 @@ Motivo:​ %1$s." "Enviado" ". " "Servidor não suportado" - "URL do Servidor" + "URL do servidor" "Configurações" "Localização compartilhada" "Saindo" "Algo deu errado" "Encontramos um problema. Tente novamente." - "Iniciando o chat…" - "Adesivo" + + "%1$d espaço" + "%1$d espaços" + + "Iniciando a conversa…" + "Figurinha" "Sucesso" "Sugestões" "Sincronizando" "Sistema" "Texto" - "Avisos de terceiros" + "Comunicados de terceiros" "Tópico" "Tópico" "Sobre o que é essa sala?" - "Não é possível descriptografar" + "Não foi possível descriptografar" "Enviado de um dispositivo inseguro" - "Você não tem acesso a esta mensagem" + "Você não tem acesso à 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" + "Parar de silenciar" + "Chamada não suportada" "Evento não suportado" "Nome do usuário" "Verificação cancelada" @@ -308,28 +336,28 @@ Motivo:​ %1$s." "Verificar usuário" "Vídeo" "Mensagem de voz" - "Esperando…" - "Aguardando esta mensagem" + "Aguardando…" + "Aguardando por esta mensagem" "Você" "A identidade de %1$s foi redefinida. %2$s" - "A identidade de %1$s em %2$s foi redefinida. %3$s" + "A identidade de %1$s %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" + "A identidade de %1$s %2$s foi redefinida. %3$s" + "Anular 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" + "Verifique este link duas vezes" "Sala denunciada" "Denunciou e deixou a sala" "Confirmação" "Erro" "Sucesso" - "Aviso" + "Alerta" "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." + "Seu servidor-casa 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" @@ -347,15 +375,16 @@ Você tem certeza de que deseja continuar?" "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." + "Criptografado por um usuário verificado previamente." "Não criptografado." - "Criptografada por um dispositivo desconhecido ou apagado." + "Criptografado 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." "🔐️ Junte-se a mim no %1$s" "Ei, fale comigo em %1$s: %2$s" - "%1$s Android" - "Rageshake para relatar um bug" + "%1$s (Android)" + "Agitar agressivamente para relatar um bug" + "Captura de tela" "%1$s: %2$s" "Opções" "Remover %1$s" @@ -364,13 +393,13 @@ Você tem certeza de que deseja continuar?" "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" + "%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." + "Você está prestes a acessar sua conta %1$s para redefinir sua identidade. Depois disso, você será levado de volta ao app." "Não consegue confirmar? Acesse sua conta para redefinir sua identidade." - "Retire a verificação e envie" + "Retirar verificação e enviar" "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" @@ -378,8 +407,9 @@ Você tem certeza de que deseja continuar?" "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" + "Editar administradores ou proprietários" + "Falha ao processar a mídia para o envio. Tente novamente." + "Não foi possível buscar os detalhes do usuário" "Mensagem em %1$s" "Expandir" "Reduzir" @@ -388,13 +418,16 @@ Você tem certeza de que deseja continuar?" "%1$s Mensagens fixadas" "Carregando mensagem…" "Ver tudo" - "Bate-papo" + "Conversa" "Compartilhar localização" "Compartilhar minha localização" "Abrir no Apple Maps" "Abrir no Google Maps" "Abrir no OpenStreetMap" - "Compartilhe esta localização" + "Compartilhar esta localização" + "Os espaços que você criou ou entrou." + "%1$s • %2$s" + "Espaços" "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." @@ -402,8 +435,8 @@ Você tem certeza de que deseja continuar?" "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" + "Você precisa verificar este dispositivo para ter acesso à mensagens históricas" + "Você não tem acesso à 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 ce5c97b78f..d6c43402e3 100644 --- a/libraries/ui-strings/src/main/res/values-pt/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pt/translations.xml @@ -2,6 +2,7 @@ "Adicionar reação: %1$s" "Avatar" + "Minimizar campo de texto da mensagem" "Eliminar" "%1$d dígito inserido" @@ -10,6 +11,7 @@ "Editar avatar" "O endereço completo será %1$s" "Detalhes de cifragem" + "Expandir campo de texto da mensagem" "Ocultar palavra-passe" "Juntar-se à chamada" "Saltar para o fundo" @@ -40,9 +42,9 @@ "Remover reação com %1$s" "Ícone da sala" "Enviar ficheiros" + "Necessária ação em tempo limitado, tens um minuto para verificares" "Mostrar palavra-passe" "Iniciar chamada" - "Necessária ação em tempo limitado" "Sala antiga" "Avatar do utilizador" "Menu de utilizador" @@ -88,6 +90,7 @@ "Ativar" "Fim da sondagem" "Inserir PIN" + "Concluir" "Esqueceu-se da palavra-passe?" "Reencaminhar" "Voltar" @@ -102,6 +105,7 @@ "Sair" "Sair da conversa" "Sair da sala" + "Sair do espaço" "Carregar mais" "Gerir conta" "Gerir dispositivos" @@ -111,7 +115,7 @@ "Agora não" "OK" "Abrir menu de contexto" - "Definições" + "Configurações" "Abrir com" "Afixar" "Resposta rápida" @@ -162,10 +166,14 @@ "Atualização disponível" "Sobre" "Política de utilização aceitável" + "Adicionar conta" + "Adicionar outra conta" "A adicionar legenda" "Configurações avançadas" "uma imagem" "Recolha e análise de dados" + "Saíste da sala" + "A tua sessão foi terminada" "Aparência" "Áudio" "Utilizadores bloqueados" @@ -177,9 +185,12 @@ "A criar sala…" "Pedido cancelado" "Saíste da sala" + "Saíste do espaço" "Convite rejeitado" "Escuro" "Erro de decifragem" + "Descrição" + "Desselecionar tudo" "Opções de programador" "ID do dispositivo" "Conversa direta" @@ -236,6 +247,7 @@ Razão: %1$s." "%1$s (%2$s)" "Sem resultados" "Sala sem nome" + "Espaço sem nome" "Não encriptado" "Desligado" "Licenças de código aberto" @@ -245,8 +257,8 @@ Razão: %1$s." "Ligação permanente" "Permissão" "Afixado" - "Por favor, verifica a tua ligação à Internet" - "Por favor, aguarde…" + "Por favor, verifica a tua ligação à internet" + "Por favor, aguarda…" "Tens a certeza que queres concluir esta sondagem?" "Sondagem: %1$s" "Total de votos: %1$s" @@ -289,18 +301,23 @@ Razão: %1$s." "Resultados da pesquisa" "Segurança" "Vista por" + "Selecionar conta" + "Selecionar tudo" "Enviar para" "A enviar…" "Falha no envio" "Enviada" ". " "Servidor não suportado" + "Servidor indisponível" "URL do servidor" "Configurações" + "Partilhar espaço" "Localização partilhada" "A terminar sessão" "Algo correu mal" - "Encontrámos um erro. Por favor, tenta novamente." + "Encontramos um erro. Por favor, tenta novamente." + "Espaço" "%1$d espaço" "%1$d espaços" @@ -335,6 +352,12 @@ Razão: %1$s." "Verifica a identidade" "Verificar utilizador" "Vídeo" + "Alta qualidade" + "Melhor qualidade, mas maior tamanho de ficheiro" + "Baixa qualidade" + "A velocidade de carregamento mais rápida e o tamanho de ficheiro mais pequeno" + "Qualidade padrão" + "Equilíbrio entre qualidade e velocidade de carregamento" "Mensagem de voz" "A aguardar…" "À espera desta mensagem" @@ -349,14 +372,23 @@ Razão: %1$s." Tens a certeza de que queres continuar?" "Verifica novamente esta ligação" + "Seleciona a qualidade predefinida dos vídeos que carregas." + "Qualidade de carregamento do vídeo" + "O tamanho máximo de ficheiro permitido é: %1$s" + "O tamanho do ficheiro é demasiado grande para ser carregado" "Sala denunciada" - "Sala denunciada e abandonada" + "Reportaste e saíste da sala" "Confirmação" "Erro" "Sucesso" "Aviso" "As tuas alterações não foram guardadas. Tens a certeza que queres voltar atrás?" "Guardar alterações?" + "O tamanho máximo de ficheiro permitido é: %1$s" + "Seleciona a qualidade do vídeo que pretendes carregar." + "Seleciona a qualidade de carregamento do vídeo" + "Pesquisar emojis" + "Já tens sessão iniciada como %1$s neste dispositivo." "Seu homeserver precisa ser atualizado para suportar o Matrix Authentication Service e a criação de conta." "Falha ao criar ligação permanente" "%1$s não foi possível carregar o mapa. Por favor, tente novamente mais tarde." @@ -372,7 +404,7 @@ Tens a certeza de que queres continuar?" "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" + "Pedimos desculpa, ocorreu um erro desconhecido" "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." @@ -407,6 +439,7 @@ Tens a certeza de que queres continuar?" "A sua mensagem não foi enviada porque %1$s não verificou todos os dispositivos" "Um ou mais dos teus dispositivos não foram verificados. Podes enviar a mensagem na mesma, ou podes cancelar por agora e tentar novamente mais tarde, depois de teres verificado todos os teus dispositivos." "A sua mensagem não foi enviada porque não verificou um ou mais dos seus dispositivos" + "Editar administradores ou proprietários" "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" @@ -426,6 +459,7 @@ Tens a certeza de que queres continuar?" "Partilhar este local" "Espaços que criaste ou nos quais entraste." "%1$s • %2$s" + "Espaço %1$s" "Espaços" "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." 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 77f1a715be..ee2533cafa 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -1,17 +1,31 @@ + "Adăugați o reacție: %1$s" + "Imagine de profil" + "Micșorați câmpul mesajului" "Ștergere" "%1$d cifră introdusă" "%1$d cifre introduse" "%1$d cifre introduse" + "Editați avatarul" + "Adresa completă va fi %1$s" + "Detalii privind criptarea" + "Extindeți câmpul mesajului" "Ascundeți parola" + "Alăturați-vă apelului" "Mergeți în jos" + "Mutați harta la locația mea" "Doar mențiuni" "Notificări dezactivate" + "Mențiuni noi" + "Mesaje noi" + "Apel în desfășurare" + "Avatarul celuilalt utilizator" "Pagina %1$d" "Pauză" + "Mesaj vocal, durată:%1$s, poziție curentă: %2$s" "Câmp PIN" "Redați" "Sondaj" @@ -24,17 +38,27 @@ "Citit de %1$s și incă %2$d" "Citit de %1$s și incă %2$d" - "Citit de%1$s" + "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 %1$s" + "Avatarul camerei" "Trimiteți fișiere" + "Acțiune cu termen limită necesară" "Afișați parola" "Începeți un apel" + "Cameră terminată" + "Avatar utilizator" "Meniu utilizator" + "Vizualizați avatarul" + "Vizualizați detalii" + "Mesaj vocal, durată: %1$s" "Înregistrați un mesaj vocal" "Opriți înregistrarea" + "Avatarul dumneavoastră" "Acceptați" - "Adăugați conversației" + "Adăugați o descriere" + "Adăugați listei de mesaje" "Înapoi" "Apel" "Anulați" @@ -47,22 +71,28 @@ "Confirmați parola" "Continuați" "Copiați" + "Copiați descrierea" "Copiați linkul" "Copiați linkul către mesaj" + "Copiați textul" "Creați" "Creați o cameră" "Dezactivați" "Dezactivați contul" "Refuzați" + "Refuzați și blocați" "Ștergeți sondajul" "Dezactivați" "Renunţare" + "Renunțați" "Efectuat" "Editați" + "Editați descrierea" "Editați sondajul" "Activați" "Închideți sondajul" "Introduceți PIN-ul" + "Finalizați" "Ați uitat parola?" "Redirecționați" "Înapoi" @@ -70,7 +100,7 @@ "Invitați" "Invitați prieteni" "Invitați prieteni în %1$s" - "Invitați persoane la %1$s" + "Invitați persoane în %1$s" "Invitații" "Alăturați-vă" "Aflați mai multe" @@ -85,6 +115,7 @@ "Nu" "Nu acum" "OK" + "Deschideți meniul contextual" "Setări" "Deschideți cu" "Fixează" @@ -92,11 +123,16 @@ "Citat" "Reacționați" "Respinge" - "Ștergeți" + "Indepărtați" + "Ștergeți descrierea" + "Ștergeți mesajul" "Răspundeți" "Răspundeți în fir" + "Raportați" "Raportați o eroare" "Raportați conținutul" + "Raportați conversația" + "Raportați camera" "Resetare" "Resetați identitatea" "Reîncercați" @@ -104,7 +140,9 @@ "Salvați" "Căutați" "Trimiteți" + "Trimiteți mesajul editat" "Trimiteți mesajul" + "Trimiteți un mesaj vocal" "Partajați" "Partajați linkul" "Afișare" @@ -120,15 +158,21 @@ "Atingeți pentru opțiuni" "Încercați din nou" "Defixeaza" - "Vedeți în cameră" + "Vizualizați" + "Vedeți în lista de mesaje" "Vedeți sursă" "Da" + "Da, încercați din nou" "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ă" + "Adăugare descriere" "Setări avansate" + "o imagine" "Analitice" + "Ați părăsit camera" + "Ați fost deconectat din sesiune." "Aspect" "Audio" "Utilizatori blocați" @@ -138,16 +182,22 @@ "Copiat în clipboard" "Drepturi de autor" "Se creează camera…" + "Cerere anulată" "Ați parăsit camera" + "Invitația a fost refuzată" "Întunecat" "Eroare de decriptare" "Opțiuni programator" "ID-ul dispozitivului" "Chat direct" "Nu mai afișa acest mesaj" + "Descărcarea a eșuat" + "Se descarcă" "(editat)" "Editare" + "Editare descriere" "* %1$s %2$s" + "Fișier gol" "Criptare" "Criptare activată" "Introduceți codul PIN" @@ -160,8 +210,11 @@ Motiv:%1$s." "Favorite" "Favorită" "Fişier" + "Fișier șters" + "Fișier salvat" "Fișier salvat în Descărcări" "Redirecționați mesajul" + "Utilizate frecvent" "GIF" "Imagine" "Ca răspuns la %1$s" @@ -169,26 +222,30 @@ Motiv:%1$s." "Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost trimisă." "Se părăsește conversația" "Deschis" + "Linie copiată în clipboard" "Linkul a fost copiat în clipboard" "Se încarcă…" + "Se încarcă…" "%d altul" "%d alții" "%d alții" - "%1$d membru" - "%1$d membri" - "%1$d membri" + "%1$d Membru" + "%1$d Membri" + "%1$d Membri" "Mesaj" "Acțiuni mesaj" "Aspectul mesajelor" - "Mesaj sters" + "Mesaj șters" "Modern" "Dezactivați sunetul" + "%1$s (%2$s)" "Niciun rezultat" "Fără nume de cameră" + "Necriptat" "Deconectat" "Licențe open source" "sau" @@ -197,6 +254,7 @@ Motiv:%1$s." "Permalink" "Permisiune" "Fixat" + "Vă rugăm să verificați conexiunea la internet" "Va rugam asteptati…" "Sunteți sigur că doriți să încheiați acest sondaj?" "Sondajul %1$s" @@ -207,13 +265,22 @@ Motiv:%1$s." "%d voturi" "%d voturi" + "Se pregăteşte…" "Politica de confidențialitate" "Cameră privată" + "Spațiu privat" "Cameră publică" + "Spațiu public" "Reacţie" "Reacții" + "Motiv" "Cheie de recuperare" "Se actualizează" + + "%1$d răspuns" + "%1$d răspunsuri" + "%1$d răspunsuri" + "Răspuns pentru %1$s" "Raportați o eroare" "Raportați o problemă" @@ -222,6 +289,11 @@ Motiv:%1$s." "Cameră" "Numele camerei" "de exemplu, numele proiectului dvs." + + "%1$d Camera" + "%1$d Camere" + "%1$d Camere" + "Modificări salvate" "Se salvează…" "Blocare ecran" @@ -233,12 +305,20 @@ Motiv:%1$s." "Se trimite…" "Trimiterea a eșuat" "Trimis" + ". " "Serverul nu este compatibil" "Adresa URL a serverului" "Setări" "Locație partajată" "Deconectare în curs" "Ceva nu a mers bine" + "Am întâmpinat o problemă. Vă rugăm să încercați din nou." + "Spațiu" + + "%1$d Spațiu" + "%1$d Spații" + "%1$d Spații" + "Se începe conversația…" "Autocolant" "Succes" @@ -253,11 +333,12 @@ Motiv:%1$s." "Nu s-a putut decripta" "Trimis de pe un dispozitiv nesigur" "Nu aveți acces la acest mesaj" - "Identitatea verificată a expeditorului s-a schimbat" + "Identitatea verificată a expeditorului a fost resetată" "Nu am putut trimite invitații unuia sau mai multor utilizatori." "Nu s-a putut trimite invitația (invitațiile)" "Deblocare" "Activați sunetul" + "Apel nesuportat" "Eveniment neacceptat" "Utilizator" "Verificare anulată" @@ -265,42 +346,76 @@ Motiv:%1$s." "Verificarea a eșuat" "Verificat" "Verificați dispozitivul" + "Verificați identitatea" + "Verificați utilizatorul" "Video" + "Calitate înaltă" + "Cea mai bună calitate, dar dimensiuni mai mari ale fișierelor" + "Calitate redusă" + "Cea mai rapidă viteză de încărcare și cea mai mică dimensiune a fișierelor" + "Calitate standard" + "Echilibru între calitate și viteza de încărcare" "Mesaj vocal" "Se aşteaptă…" "Mesaj în așteptare" "Dumneavoastră" - "Identitatea lui %1$s pare să se fi schimbat. %2$s" - "Identitatea %2$s a lui %1$s pare să se fi schimbat. %3$s" + "Identitatea lui %1$s a fost resetată. %2$s" + "Identitatea %2$s a lui %1$s a fost resetată. %3$s" "(%1$s)" + "Identitatea lui %1$s a fost resetată." + "Identitatea %2$s a lui %1$s a fost resetată. %3$s" + "Retrageți verificarea" + "Linkul %1$s vă redirecționează către un alt site %2$s + +Sunteți sigur că doriți să continuați?" + "Verificați din nou acest link" + "Selectați calitatea implicită a videoclipurilor pe care le încărcați." + "Calitatea încărcării videoclipurilor" + "Dimensiunea maximă permisă pentru fișiere este: %1$s" + "Dimensiunea fișierului este prea mare pentru a fi încărcat." + "Cameră raportată" + "Camera a fost raportată si parasită" "Confirmare" "Eroare" "Succes" "Avertisment" "Modificările dumneavoastră nu au fost salvate. Sunteți sigur că doriți să vă întoarceți?" "Salvați modificările?" + "Dimensiunea maximă permisă pentru fișiere este: %1$s" + "Selectați calitatea videoclipului pe care doriți să îl încărcați." + "Selectați calitatea de încărcare a videoclipurilor" "Serverul dumneavoastră trebuie actualizat pentru a suporta serviciul de autentificare Matrix și crearea de conturi." "Crearea permalink-ului a eșuat" "%1$s nu a putut încărca harta. Vă rugăm să încercați din nou mai târziu." "Încărcarea mesajelor a eșuat" "%1$s nu a putut accesa locația dumneavoastră. Vă rugăm să încercați din nou mai târziu." "Trimiterea mesajului vocal nu a reușit." + "Camera nu mai există sau invitația nu mai este valabilă." "Mesajul nu a fost găsit" "%1$s nu are permisiuni pentru a accesa locația dumneavoastră. Puteți permite accesul în Setări." "%1$s nu are permisiuni pentru a accesa locația dumneavoastră. Permiteți accesul mai jos." "%1$s nu are permisiunea de a vă accesa microfonul. Permiteți accesul pentru a înregistra un mesaj vocal." + "Acest lucru se poate datora unor probleme de rețea sau de server." + "Această adresă de cameră există deja. Încercați să editați câmpul adresei camerei sau să schimbați numele camerei." + "Unele caractere nu sunt permise. Sunt acceptate doar literele, cifrele și următoarele simboluri ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "Unele mesaje nu au fost trimise" "Ne pare rău, a apărut o eroare" + "Expeditorul evenimentului nu corespunde proprietarului dispozitivului care l-a trimis." "Autenticitatea acestui mesaj criptat nu poate fi garantată pe acest dispozitiv." "Criptat de un utilizator verificat anterior." "Necriptat" "Criptat de un dispozitiv necunoscut sau șters." "Criptat de un dispozitiv care nu este verificat de proprietarul său." "Criptat de un utilizator neverificat." - "🔐️ Alăturați-vă mie pe %1$s" + "🔐️ Alăturați-vă mie în %1$s" "Hei, vorbește cu mine pe %1$s: %2$s" "%1$s Android" "Rageshake pentru a raporta erori" + "Captură de ecran" + "%1$s: %2$s" + "Opțiuni" + "Ștergeți %1$s" + "Setări" "Selectarea fișierelor media a eșuat, încercați din nou." "Apăsați pe un mesaj și alegeți \"%1$s\" pentru a-l include aici." "Fixați mesajele importante, astfel încât să poată fi descoperite cu ușurință" @@ -314,14 +429,19 @@ Motiv:%1$s." "Nu puteți confirma? Accesați contul dvs. pentru a vă reseta identitatea." "Retrageți verificarea și trimiteți" "Puteți să vă retrageți verificarea și să trimiteți acest mesaj oricum, sau puteți anula pentru moment și să încercați din nou mai târziu după reverificarea lui %1$s." - "Mesajul dvs. nu a fost trimis deoarece identitatea verificată a lui %1$s s-a schimbat" + "Mesajul dumneavoastră nu a fost trimis deoarece identitatea verificată a lui %1$s s-a schimbat" "Trimiteți mesajul oricum" "%1$s utilizează unul sau mai multe dispozitive neverificate. Puteți trimite mesajul oricum sau puteți anula pentru moment și puteți încerca din nou mai târziu, după ce %2$s își va verifica toate dispozitivele." "Mesajul dvs. nu a fost trimis deoarece %1$s nu si-a verificat toate dispozitivele" "Unul sau mai multe dispozitive nu sunt verificate. Puteți trimite mesajul oricum sau puteți anula deocamdată și încercați din nou mai târziu după ce ați verificat toate dispozitivele." "Mesajul dumneavoastră nu a fost trimis deoarece nu ați verificat unul sau mai multe dispozitive" + "Editați administratorii sau proprietarii" "Procesarea datelor media a eșuat, vă rugăm să încercați din nou." "Nu am putut găsi detaliile utilizatorului" + "Mesaj în %1$s" + "Extindeți" + "Reduceți" + "Deja vizualizați această cameră!" "%1$s din %2$s" "%1$s Mesaje fixate" "Se încarcă mesajul…" @@ -333,10 +453,18 @@ Motiv:%1$s." "Deschideți în Google Maps" "Deschideți în OpenStreetMap" "Distribuiți această locație" + "Spații pe care le-ați creat sau la care v-ați alăturat." + "%1$s • %2$s" + "Spații" "Mesajul nu a fost trimis deoarece identitatea verificată a lui %1$s s-a schimbat." "Mesajul nu a fost trimis deoarece %1$s nu a verificat toate dispozitivele." "Mesajul nu a fost trimis deoarece nu ați verificat unul sau mai multe dispozitive." "Locație" "Versiunea: %1$s (%2$s)" "ro" + "Messaje anterioare nu sunt disponibile pe acest dispozitiv." + "Trebuie să verificați acest dispozitiv pentru a avea acces la mesajele anterioare." + "Nu aveți acces la acest mesaj" + "Nu s-a putut decripta mesajul" + "Acest mesaj a fost blocat fie pentru că nu ați verificat dispozitivul, fie pentru că expeditorul trebuie să vă verifice identitatea." 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 ff06c09efb..d57db4ce60 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -8,15 +8,19 @@ "Ведено %1$d цифры" "Введено много цифр" + "Изменить аватар" + "Полный адрес %1$s" "Сведения о шифровании" "Скрыть пароль" "Присоединиться к звонку" "Перейти вниз" + "Переместить карту на мое местоположение" "Только упоминания" "Звук отключен" "Новые упоминания" "Новые сообшения" "Текущий вызов" + "Аватар другого пользователя" "Страница %1$d" "Приостановить" "Голосовое сообщение, длительность: %1$s, текущая позиция: %2$s" @@ -35,14 +39,20 @@ "Прочитано %1$s" "Нажмите, чтобы показать все" "Удалить реакцию с %1$s" + "Удалить реакцию %1$s" + "Аватар комнаты" "Отправить файлы" + "Требуется срочное действие" "Показать пароль" "Начать звонок" + "Аватар пользователя" "Меню пользователя" + "Просмотреть аватар" "Подробнее" "Голосовое сообщение, продолжительность: %1$s" "Записать голосовое сообщение." "Остановить запись" + "Ваш аватар" "Разрешить" "Добавить подпись" "Добавить в хронологию" @@ -79,6 +89,7 @@ "Включить" "Завершить опрос" "Введите PIN-код" + "Завершить" "Забыли пароль?" "Переслать" "Вернуться" @@ -101,6 +112,7 @@ "Нет" "Не сейчас" "Ок" + "Открыть контекстное меню" "Открыть настройки" "Открыть с помощью" "Закрепить" @@ -125,7 +137,9 @@ "Сохранить" "Поиск" "Отправить" + "Отправить изменённое сообщение" "Отправить сообщение" + "Отправить голосовое сообщение" "Поделиться" "Поделиться ссылкой" "Показать" @@ -141,6 +155,7 @@ "Нажмите для просмотра вариантов" "Повторить попытку" "Открепить" + "Просмотр" "Просмотр в хронологии" "Показать источник" "Да" @@ -151,7 +166,9 @@ "Политика допустимого использования" "Добавление подписи" "Дополнительные настройки" + "изображение" "Аналитика" + "Вы покинули комнату" "Внешний вид" "Аудио" "Заблокированные пользователи" @@ -244,14 +261,22 @@ "%d голоса" "%d голосов" + "Подготовка…" "Политика конфиденциальности" "Частная комната" + "Приватное пространство" "Общедоступная комната" + "Публичное пространство" "Реакция" "Реакции" "Причина" "Ключ восстановления" "Обновление…" + + "%1$d ответ" + "%1$d ответа" + "%1$d ответов" + "Отвечает на %1$s" "Сообщить об ошибке" "Сообщить о проблеме" @@ -271,6 +296,7 @@ "Отправка…" "Сбой отправки" "Отправлено" + ". " "Сервер не поддерживается" "Адрес сервера" "Настройки" @@ -278,6 +304,7 @@ "Выход…" "Что-то пошло не так" "Мы столкнулись с проблемой. Пожалуйста, попробуйте еще раз." + "Подпространство" "Чат запускается…" "Стикер" "Успешно" @@ -308,20 +335,30 @@ "Подтвердить личность" "Подтвердить пользователя" "Видео" + "Высокое качество" + "Лучшее качество, но больший размер файла" + "Низкое качество" + "Быстрая скорость загрузки и меньший размер файла" + "Стандартное качество" + "Сочетание качества и скорости загрузки" "Голосовое сообщение" "Ожидание…" "Ожидание ключа расшифровки" "Вы" "Идентификатор %1$s изменился. %2$s" - "Пользователь %1$s сменил имя пользователя на %2$s. %3$s" + "Пользователь %1$s сменил имя на %2$s. %3$s" "(%1$s)" "%1$s была сброшена." - "%1$s’s %2$s подтвержденная личность изменилась. %3$s" + "Пользователь %1$s сменил имя на %2$s. %3$s" "Вывод верификации" "Ссылка %1$s ведет вас на другой сайт %2$s Вы действительно хотите продолжить?" "Перепроверьте эту ссылку" + "Выберите качество загружаемых видео по умолчанию." + "Качество загружаемого видео" + "Максимально допустимый размер файла: %1$s" + "Размер файла слишком большой для загрузки." "Сообщение о комнате" "Пожаловался и покинул комнату" "Подтверждение" @@ -330,6 +367,7 @@ "Предупреждение" "Изменения не сохранены. Вы действительно хотите вернуться?" "Сохранить изменения?" + "Выберите качество видео, которое вы хотите загрузить." "Выберите качество загружаемого видео" "Ваш домашний сервер необходимо обновить, чтобы он поддерживал Matrix Authentication Service и создание учётных записей." "Не удалось создать постоянную ссылку" @@ -337,6 +375,7 @@ "Не удалось загрузить сообщения" "%1$s не удалось получить доступ к вашему местоположению. Пожалуйста, повторите попытку позже." "Не удалось загрузить голосовое сообщение." + "Комната больше не существует или приглашение не действительно." "Сообщение не найдено" "У %1$s нет разрешения на доступ к вашему местоположению. Вы можете разрешить доступ в Настройках." "У %1$s нет разрешения на доступ к вашему местоположению. Разрешите доступ ниже." @@ -346,6 +385,7 @@ "Некоторые символы не допускаются. Поддерживаются только буквы, цифры и следующие символы! $ & \'() * +/; =? @ [] - . _" "Некоторые сообщения не были отправлены" "Извините, произошла ошибка" + "Отправитель события и владелец устройства не совпадают." "Подлинность этого зашифрованного сообщения не может быть гарантирована на этом устройстве." "Зашифровано ранее проверенным пользователем." "Не зашифровано." @@ -356,6 +396,11 @@ "Привет, поговори со мной по %1$s: %2$s" "%1$s Android" "Встряхните устройство, чтобы сообщить об ошибке" + "Скриншот" + "%1$s: %2$s" + "Параметры" + "Удалить %1$s" + "Настройки" "Не удалось выбрать носитель, попробуйте еще раз." "Нажмите на сообщение и выберите “%1$s”, чтобы добавить его сюда." "Закрепите важные сообщения, чтобы их можно было легко найти" @@ -375,9 +420,13 @@ "Ваше сообщение не было отправлено, потому что %1$s не проверил одно или несколько устройств" "Одно или несколько ваших устройств не проверены. Вы можете отправить сообщение в любом случае или отменить его пока и повторить попытку позже, проверив все свои устройства." "Ваше сообщение не было отправлено, поскольку вы не подтвердили одно или несколько своих устройств." + "Редактировать роль владельца и администратора" "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." "Не удалось получить данные о пользователе" "Сообщение в %1$s" + "Развернуть" + "Уменьшить" + "Эта комната уже просматривается!" "%1$s из %2$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 4c542069be..baee4b4367 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -42,9 +42,9 @@ "Odstrániť reakciu s %1$s" "Obrázok miestnosti" "Odoslať súbory" + "Vyžaduje sa časovo obmedzená akcia" "Zobraziť heslo" "Začať hovor" - "Vyžaduje sa časovo obmedzená akcia" "Opustená miestnosť" "Profilový obrázok" "Používateľské menu" @@ -90,6 +90,7 @@ "Povoliť" "Ukončiť anketu" "Zadajte PIN" + "Dokončiť" "Zabudnuté heslo?" "Preposlať" "Ísť späť" @@ -168,6 +169,8 @@ "Pokročilé nastavenia" "obrázok" "Analytika" + "Opustili ste miestnosť" + "Boli ste odhlásení zo relácie." "Vzhľad" "Zvuk" "Blokovaní používatelia" @@ -308,6 +311,7 @@ Dôvod: %1$s." "Odhlasovanie" "Niečo sa pokazilo" "Vyskytol sa problém. Skúste to prosím znova." + "Priestor" "%1$d priestor" "%1$d priestory" 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 8187c4b3a5..06b51baf80 100644 --- a/libraries/ui-strings/src/main/res/values-sv/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sv/translations.xml @@ -8,9 +8,12 @@ "%1$d siffror angivna" "Redigera avatar" + "Den fullständiga adressen kommer att vara %1$s" + "Krypteringsdetaljer" "Dölj lösenord" "Anslut till samtal" "Hoppa till botten" + "Flytta kartan till min plats" "Endast omnämningar" "Tystad" "Nya omnämnanden" @@ -37,8 +40,10 @@ "Ta bort reaktion med %1$s" "Rumsavatar" "Skicka filer" + "Tidsbegränsad åtgärd krävs" "Visa lösenord" "Starta ett samtal" + "Gravstensmärkt rum" "Användaravatar" "Användarmeny" "Visa avatar" @@ -83,6 +88,7 @@ "Aktivera" "Avsluta omröstning" "Ange PIN-kod" + "Slutför" "Glömt lösenordet?" "Vidarebefordra" "Gå tillbaka" @@ -161,6 +167,8 @@ "Avancerade inställningar" "en bild" "Analysdata" + "Du lämnade rummet" + "Du loggades ut ur sessionen" "Utseende" "Ljud" "Blockerade användare" @@ -250,9 +258,12 @@ Anledning:%1$s." "%d röst" "%d röster" + "Förbereder …" "Integritetspolicy" "Privat rum" + "Privat utrymme" "Offentligt rum" + "Offentligt utrymme" "Reaktion" "Reaktioner" "Orsak" @@ -270,6 +281,10 @@ Anledning:%1$s." "Rum" "Rumsnamn" "t.ex. ditt projektnamn" + + "%1$d Rum" + "%1$d Rum" + "Sparade ändringar" "Sparar" "Skärmlås" @@ -289,6 +304,11 @@ Anledning:%1$s." "Loggar ut" "Något gick fel" "Vi stötte på ett problem. Vänligen försök igen." + "Utrymme" + + "%1$d Utrymme" + "%1$d Utrymmen" + "Startar chatt …" "Dekal" "Lyckades" @@ -319,6 +339,12 @@ Anledning:%1$s." "Verifiera identitet" "Verifiera användare" "Video" + "Hög kvalitet" + "Bästa kvalitet men större filstorlek" + "Låg kvalitet" + "Snabbast uppladdningshastighet och minsta filstorlek" + "Standardkvalitet" + "Balans mellan kvalitet och uppladdningshastighet" "Röstmeddelande" "Väntar …" "Väntar på detta meddelande" @@ -333,6 +359,10 @@ Anledning:%1$s." Är du säker på att du vill fortsätta?" "Dubbelkolla den här länken" + "Välj standardkvalitet för videor du laddar upp." + "Videouppladdningskvalitet" + "Maximal tillåten filstorlek är: %1$s" + "Filen är för stor för att laddas upp." "Rum anmält" "Anmälde och lämnade rummet" "Bekräftelse" @@ -341,6 +371,9 @@ Anledning:%1$s." "Varning" "Dina ändringar har inte sparats. Är du säker på att du vill gå tillbaka?" "Spara ändringar?" + "Den maximala tillåtna filstorleken är: %1$s" + "Välj kvaliteten på videon du vill ladda upp." + "Välj videouppladdningskvalitet" "Din hemserver måste uppgraderas för att stödja Matrix Authentication Service och skapande av konto." "Misslyckades att skapa permalänken" "%1$s kunde inte ladda kartan. Vänligen försök igen senare." @@ -368,6 +401,7 @@ Anledning:%1$s." "Hallå, prata med mig på %1$s: %2$s" "%1$s Android" "Raseriskaka för att rapportera bugg" + "Skärmdump" "%1$s: %2$s" "Alternativ" "Ta bort %1$s" @@ -390,6 +424,7 @@ Anledning:%1$s." "Ditt meddelande skickades inte eftersom %1$s inte har verifierat alla enheter" "En eller flera av dina enheter är overifierade. Du kan skicka meddelandet ändå, eller så kan du avbryta nu och försöka igen senare efter att du har verifierat alla dina enheter." "Ditt meddelande skickades inte eftersom du inte har verifierat en eller flera av dina enheter" + "Redigera administratörer eller ägare" "Misslyckades att bearbeta media för uppladdning, vänligen pröva igen." "Kunde inte hämta användarinformation" "Meddelande i %1$s" @@ -407,6 +442,9 @@ Anledning:%1$s." "Öppna i Google Maps" "Öppna i OpenStreetMap" "Dela den här platsen" + "Utrymmen som du har skapat eller gått med i." + "%1$s • %2$s" + "Utrymmen" "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." diff --git a/libraries/ui-strings/src/main/res/values-tr/translations.xml b/libraries/ui-strings/src/main/res/values-tr/translations.xml index d4db6202fd..c72f5a6139 100644 --- a/libraries/ui-strings/src/main/res/values-tr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-tr/translations.xml @@ -1,11 +1,13 @@ + "Profil resmi" "Sil" "%1$d basamak girildi" "%1$d basamak girildi" "Şifreyi gizle" + "Aramaya katıl" "Aşağıya atla" "Yalnızca bahsetmeler" "Sessiz" @@ -59,6 +61,7 @@ "Anketi Sil" "Devre dışı" "Vazgeç" + "Kapat" "Bitti" "Düzenle" "Açıklamayı düzenle" @@ -145,7 +148,9 @@ "Panoya kopyalandı" "Telif Hakkı" "Oda yaratmak…" + "İstek iptal edildi" "Sol oda" + "Davet reddedildi" "Koyu" "Şifre çözme hatası" "Geliştirici seçenekleri" @@ -158,6 +163,7 @@ "Düzenleme" "Açıklamayı düzenleme" "* %1$s %2$s" + "Boş dosya" "Şifreleme" "Şifreleme etkin" "PIN\'inizi girin" @@ -182,6 +188,7 @@ Neden: %1$s." "Bu Matrix Kimliği bulunamıyor, bu nedenle davet alınmayabilir." "Odadan ayrılma" "Aydınlık" + "Metin panoya kopyalandı" "Bağlantı panoya kopyalandı" "Yükleniyor…" "Daha fazla yükleniyor…" @@ -191,7 +198,7 @@ Neden: %1$s." "%1$d üye" - "%1$d üye" + "%1$d üyeleri" "Mesaj" "Mesaj eylemleri" @@ -202,6 +209,7 @@ Neden: %1$s." "%1$s (%2$s)" "Sonuç yok" "Oda adı yok" + "Şifrelenmemiş" "Çevrimdışı" "Açık kaynak lisansları" "veya" @@ -285,12 +293,16 @@ Neden: %1$s." "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 kişinin kimliği değişti. %2$s" + "%1$s\'ın %2$s kimliği değişti. %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" + "%1$s bağlantısı seni başka bir siteye yönlendiriyor %2$s + +Devam etmek istediğinizden emin misiniz?" + "Bu bağlantıyı tekrardan kontrol edin" "Onaylama" "Hata" "Başarılı" 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 83067641be..1afd19dc3a 100644 --- a/libraries/ui-strings/src/main/res/values-uk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uk/translations.xml @@ -2,6 +2,7 @@ "Додати реакцію: %1$s" "Аватар" + "Згорнути поле тексту повідомлення" "Видалити" "Введена %1$d цифра" @@ -11,6 +12,7 @@ "Редагувати аватар" "Повна адреса буде %1$s" "Подробиці шифрування" + "Розгорнути текстове поле повідомлення" "Cховати пароль" "Приєднатися до виклику" "Перейти вниз" @@ -42,9 +44,10 @@ "Прибрати реакцію %1$s" "Аватар кімнати" "Надіслати файли" + "Необхідно виконати дію, обмежену в часі, у вас є одна хвилина для верифікації" "Показати пароль" "Розпочати виклик" - "Необхідно виконати дію, обмежену в часі" + "Кімната більше не використовується" "Аватар користувача" "Меню користувача" "Переглянути аватар" @@ -164,10 +167,14 @@ "Доступне оновлення" "Відомості" "Політика прийнятного використання" + "Додати обліковий запис" + "Додати ще один обліковий запис" "Додавання підпису" "Додаткові налаштування" "зображення" "Аналітика" + "Ви вийшли з кімнати" + "Ви вийшли з сеансу" "Тема" "Аудіо" "Заблоковані користувачі" @@ -296,6 +303,7 @@ "Результати пошуку" "Безпека" "Переглянули" + "Вибрати обліковий запис" "Надіслати до" "Надсилання…" "Не вдалося надіслати" @@ -364,7 +372,10 @@ Ви впевнені, що хочете продовжити?" "Уважно перевірте це посилання" + "Вибір усталеної якості вивантажуваних відео." "Якість вивантаження відео" + "Максимально дозволений розмір файлу: %1$s" + "Розмір файлу завеликий для вивантаження" "Скаргу на кімнату надіслано" "Поскаржитися та вийти з кімнати" "Підтвердження" @@ -373,6 +384,11 @@ "Попередження" "Внесені зміни не збережено. Ви впевнені, що хочете повернутися?" "Зберегти зміни?" + "Максимально дозволений розмір файлу: %1$s" + "Виберіть якість відео, яке ви хочете вивантажити." + "Виберіть якість вивантажуваного відео" + "Пошук емодзі" + "Ви вже ввійшли на цьому пристрої як %1$s." "Ваш домашній сервер потрібно оновити, щоб він підтримував службу автентифікації Matrix і створення облікових записів." "Не вдалося створити постійне посилання" "%1$s не може завантажити мапу. Повторіть спробу пізніше." 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 62d216c239..4b6bb6a9f5 100644 --- a/libraries/ui-strings/src/main/res/values-uz/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uz/translations.xml @@ -8,6 +8,7 @@ "%1$d ta raqam kiritildi" "Avatarni tahrirlash" + "To\'liq manzil %1$s bo\'ladi" "Shifrlash tafsilotlari" "Parolni yashirish" "Qoʻngʻiroqga qoʻshilish" @@ -15,6 +16,7 @@ "Xaritani mening joylashuvimga o\'tkazish" "Faqat eslatmalar" "Ovozsiz" + "Yangi eslatmalar" "Yangi xabarlar" "Davom etayotgan qo\'ng\'iroq" "Boshqa foydalanuvchining avatari" @@ -25,15 +27,22 @@ "O\'ynang" "So\'ro\'vnoma" "So‘rovnoma yakunlandi" + "%1$s bilan munosabat bildiring" "Boshqa hisbelgilar bilan munosabat bildiring" + "%1$s va %2$s bilan oʻqish" + + "%1$s va %2$d boshqa kishilar tomonidan oʻqildi" + "%1$s va %2$d boshqa kishilar tomonidan oʻqildi" + + "Muallif: %1$s bilan oʻqish" "Hammasini ko\'rsatish uchun bosing" "Reaktsiyani olib tashlang: %1$s" "%1$s bilan reaktsiyani olib tashlang" "Xona avatari" "Fayllarni yuborish" + "Vaqt cheklangan harakat talab qilinadi" "Parolni ko\'rsatish" "Qoʻngʻiroqni boshlash" - "Vaqt cheklangan harakat talab qilinadi" "Arxivlangan xona" "Foydalanuvchi avatari" "Foydalanuvchi menyusi" @@ -44,6 +53,7 @@ "Yozishni to\'xtatish" "Sizning avataringiz" "Qabul qiling" + "Sarlavha qo\'shing" "Vaqt jadvaliga qo\'shing" "Orqaga" "Qoʻngʻiroq" @@ -57,6 +67,7 @@ "Parolni tasdiqlang" "Davom etish" "Nusxa" + "Sarlavhani nusxalash" "Havolani nusxalash" "Havolani xabaraga nusxalash" "Matnni nusxalash" @@ -66,11 +77,13 @@ "Hisobni faolsizlantirish" "Rad etish" "Rad etish va bloklash" + "So‘rovnomani o‘chirish" "Oʻchirish" "Bekor qilish" "Bekor qilish" "Bajarildi" "Tahrirlash" + "Sarlavhani tahrirlash" "So‘rovnomani tahrirlash" "Yoqish" "So‘rovnomani tugatish" @@ -100,18 +113,23 @@ "Kontekst menyusini oching" "Sozlamalar" "Bilan oching" + "Qadash" "Tez javob" "Iqtibos" "Reaksiya qilish" "Rad etish" - "Ochirish" + "Olib tashlash" + "Sarlavhani olib tashlash" "Xabarni olib tashlash" "Javob berish" "Mavzuda javob berish" + "Shikoyat qilish" "Xato haqida xabar berish" "Tarkib haqida xabar berish" "Suhbat haqida shikoyat bering" + "Xona ustidan shikoyat qilish" "Boshlangʻich holatiga qaytarish" + "Shaxsiyatni tiklash" "Qayta urinish" "Shifrni ochishni qayta urinish" "Saqlash" @@ -134,6 +152,7 @@ "Rasmga olmoq" "Variantlar uchun bosing" "Qayta urinib ko\'ring" + "Olib tashlash" "Ko\'rish" "Vaqt jadvalida koʻrish" "Manbani korish" @@ -143,6 +162,7 @@ "Yangilash mavjud" "Haqida" "Qabul qilinadigan foydalanish siyosati" + "Sarlavha qoʻshish" "Kengaytirilgan sozlamalar" "rasm" "Analitika" @@ -168,6 +188,7 @@ "Yuklab olinmoqda" "(tahrirlangan)" "Tahrirlash" + "Sarlavhani tahrirlash" "*%1$s%2$s" "Bo\'sh fayl" "Shifrlash" @@ -198,6 +219,10 @@ Sababi:%1$s." "Havola vaqtinchalik xotiraga nusxalandi" "Yuklanmoqda…" "Batafsil yuklanmoqda…" + + "%d boshqalar" + "%d boshqalar" + "%1$d a\'zo" "%1$d ishtirokchilar" @@ -219,6 +244,7 @@ Sababi:%1$s." "Odamlar" "Doimiy havola" "Ruxsat" + "Qadalgan" "Internet ulanishingizni tekshiring" "Iltimos kuting…" "Haqiqatan ham bu soʻrovnomani tugatmoqchimisiz?" @@ -232,6 +258,7 @@ Sababi:%1$s." "Tayyorlanmoqda…" "Maxfiylik siyosati" "Shaxsiy xona" + "Jamoat xonasi" "Reaktsiya" "reaksiyalar" "Sabab" @@ -261,6 +288,7 @@ Sababi:%1$s." "Server URL manzili" "Sozlamalar" "Joylashuvi ulashildi" + "Chiqish" "Nimadir xato ketdi" "Muammoga duch keldik. Iltimos, qayta urinib koʻring." "Chat boshlanmoqda…" @@ -277,6 +305,7 @@ Sababi:%1$s." "Shifrni ochish imkonsiz" "Xavfsiz boʻlmagan qurilmadan yuborilgan" "Sizni ushbu xabarga ruxsatingiz yoʻq" + "Yuboruvchining tasdiqlangan shaxsi qayta tiklandi" "Takliflarni bir yoki bir nechta foydalanuvchiga yuborib bo‘lmadi." "Taklif(lar)ni yuborib bo‘lmadi" "Qulfni ochish" @@ -285,36 +314,79 @@ Sababi:%1$s." "Foydalanuvchi nomi" "Tasdiqlash bekor qilindi" "Tasdiqlash yakunlandi" + "Tasdiqlanmadi" + "Tasdiqlangan" + "Qurilmani tasdiqlash" + "Shaxsni tasdiqlash" "Video" "Ovozli xabar" "Kutilmoqda…" "Ushbu xabarni kutilmoqda" + "Siz" + "%1$sning shaxsi qayta tiklandi.%2$s" + "(%1$s )" "Tasdiqlash" "Xato" "Muvaffaqiyat" "Ogohlantirish" + "Oʻzgarishlar saqlanmadi. Haqiqatan ham orqaga qaytmoqchimisiz?" + "O‘zgartirishlarni saqlaysizmi?" + "Matrix autentifikatsiya xizmati va hisob yaratish imkoniyatini qo‘llab-quvvatlash uchun uy serveringizni yangilash talab etiladi." "Doimiy havola yaratilmadi" "%1$sxaritani yuklay olmadi. Iltimos keyinroq qayta urinib ko\'ring." "Xabarlar yuklanmadi" "%1$sjoylashuvingizga kira olmadi. Iltimos keyinroq qayta urinib ko\'ring." + "Ovozli xabaringizni yuklashda xatolik roʻy berdi." + "Xabar topilmadi" "%1$sjoylashuvingizga kirishga ruxsati yo\'q. Sozlamalar orqali kirishni yoqishingiz mumkin." "%1$sjoylashuvingizga kirishga ruxsati yo\'q. Quyida kirishni yoqing." "%1$smikrofoningizga kirish ruxsatiga ega emas. Ovozli xabar yozish uchun ruxsatni yoqing." "Bazi xabarlar yuborilmagan" "Kechirasiz, xatolik yuz berdi" + "Bu qurilmada shifrlangan xabarning haqiqiyligini kafolatlash imkonsiz." + "Avval tasdiqlangan foydalanuvchi tomonidan shifrlangan." + "Shifrlanmagan" + "Nomaʼlum yoki oʻchirib tashlangan qurilma tomonidan shifrlangan." + "Egasi tasdiqlamagan qurilma tomonidan shifrlangan." + "Tasdiqlanmagan foydalanuvchi tomonidan shifrlangan." "🔐️ Menga qo\'shiling%1$s" "Hey, men bilan gaplash%1$s :%2$s" "%1$sAndroid" "Xato haqida xabar berish uchun G\'azablanish" "Media tanlash jarayonida xatolik yuz berdi, qayta urinib ko\'ring" + "Xabarni bosib, bu yerga kiritish uchun \"%1$s\"-ni tanlang." + "Muhim xabarlarni osongina topish uchun qadang" + + "%1$d ta qadalgan xabar" + "%1$d ta qadalgan xabar" + + "Qadalgan xabarlar" + "Shaxsingizni qayta o‘rnatish uchun %1$s hisobingizga kirishingiz kerak. Shundan so‘ng, avtomatik ravishda ilovaga qaytarilasiz." + "Tasdiqlanmadimi? Shaxsingizni tiklash uchun hisobingizga kiring." + "Tasdiqlashni olib tashlang va yuboring" + "Siz tasdiqlashni bekor qilib, bu xabarni baribir yuborishingiz yoki hozircha to‘xtatib, %1$sʼni qayta tasdiqlagandan so‘ng keyinroq yana urinib ko‘rishingiz mumkin." + "%1$sning tasdiqlangan shaxsiy ma’lumotlari qayta o‘rnatilganligi tufayli xabaringiz jo‘natilmadi" + "Baribir xabar yuborilsin" + "%1$s tasdiqlanmagan bir yoki bir nechta qurilmadan foydalanmoqda. Siz xabarni baribir yuborishingiz mumkin yoki hozircha bekor qilib, %2$s barcha qurilmalarini tasdiqlagunga qadar kutib, keyinroq qayta urinishingiz mumkin." + "%1$s barcha qurilmalarni tasdiqlamagani uchun xabaringiz yuborilmadi" + "Bir yoki bir nechta qurilmangiz tasdiqlanmagan. Xabarni istalgancha yuborishingiz yoki hozircha bekor qilishingiz va barcha qurilmalaringizni tasdiqlaganingizdan keyin qayta urinishingiz mumkin." + "Xabaringiz yuborilmadi, chunki bir yoki bir nechta qurilmangizni tasdiqlamagansiz" "Mediani yuklab bo‘lmadi, qayta urinib ko‘ring." "Foydalanuvchi tafsilotlarini olinmadi" + "%1$sʼdan %2$s" + "%1$s ta qadalgan xabar" + "Xabar yuklanmoqda…" + "Barchasini koʻrish" + "Chat" "Joylashuvni ulashish" "Joylashuvimni ulashing" "Apple Mapsda oching" "Google Mapsda oching" "OpenStreetMapda oching" "Bu joylashuvni ulashing" + "Xabar yuborilmadi, chunki %1$sʼning tasdiqlangan identifikatori asliga qaytarildi." + "Xabar yuborilmadi, chunki %1$s barcha qurilmalarni tasdiqlamagan." + "Xabaringiz yuborilmadi, chunki siz bir yoki bir nechta qurilmangizni tasdiqlamagan ekansiz." "Joylashuv" "Versiya:%1$s (%2$s )" "en" 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 5e513419d3..a093b237fa 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 @@ -2,6 +2,7 @@ "新增反應:%1$s" "大頭貼" + "最小化訊息文字欄位" "刪除" "已輸入 %1$d 個位數" @@ -9,6 +10,7 @@ "編輯大頭照" "完整地址為 %1$s" "加密詳細資訊" + "展開訊息文字欄位" "隱藏密碼" "加入通話" "跳至底部" @@ -38,9 +40,9 @@ "移除反應 %1$s" "聊天室大頭照" "傳送檔案" + "需要限時動作,您有一分鐘可以驗證" "顯示密碼" "開始通話" - "需要限時動作" "墓碑聊天室" "使用者大頭照" "使用者選單" @@ -86,6 +88,7 @@ "啟用" "結束投票" "輸入 PIN 碼" + "結束" "忘記密碼?" "轉寄" "返回" @@ -100,6 +103,7 @@ "離開" "離開對話" "離開聊天室" + "離開空間" "載入更多" "管理帳號" "管理裝置" @@ -160,10 +164,14 @@ "可升級" "關於" "可接受使用政策" + "新增帳號" + "新增其他帳號" "新增標題" "進階設定" "影像" "分析" + "您離開了聊天室" + "您已登出工作階段" "外觀" "音訊" "封鎖的使用者" @@ -175,9 +183,11 @@ "正在建立聊天室…" "請求已取消" "已離開聊天室" + "離開空間" "邀請被拒絕" "深色" "解密錯誤" + "描述" "開發者選項" "裝置 ID" "私訊" @@ -282,18 +292,21 @@ "搜尋結果" "安全性" "已讀" + "選取帳號" "傳送給" "傳送中…" "傳送失敗" "已傳送" ". " "伺服器不支援" + "無法連線至伺服器" "伺服器 URL" "設定" "位置分享" "正在登出" "有錯誤發生" "我們了遇到了問題。請再試一次。" + "空間" "%1$d 個空間" @@ -327,6 +340,12 @@ "驗證身份" "驗證使用者" "影片" + "高品質" + "品質最佳但檔案較大" + "低品質" + "最快的上傳速度且檔案最小" + "標準品質" + "品質與上傳速度的平衡" "語音訊息" "等待中…" "等待此則訊息" @@ -341,6 +360,10 @@ 您確定您想要繼續嗎?" "仔細檢查此連結" + "選取您上傳的視訊預設品質。" + "視訊上傳品質" + "允許的最大檔案大小為:%1$s" + "檔案太大,無法上傳" "聊天室已回報" "回報並離開聊天室" "確認" @@ -349,6 +372,11 @@ "警告" "變更尚未儲存,您確定要返回嗎?" "是否儲存變更?" + "最大允許的檔案大小為:%1$s" + "選取您要上傳的視訊的品質。" + "選取視訊上傳品質" + "搜尋表情符號" + "您已在此裝置上以 %1$s 的身份登入。" "您的家伺服器需要升級才能支援 Matrix Authentication Service 與帳號建立。" "無法建立永久連結" "%1$s無法載入地圖。請稍後再試。" @@ -398,6 +426,7 @@ "未傳送您的訊息,因為 %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 748eb11254..951675ce6d 100644 --- a/libraries/ui-strings/src/main/res/values-zh/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh/translations.xml @@ -1,18 +1,24 @@ + "添加表情符号:%1$s" "头像" + "最小化消息文本框" "删除" "已输入 %1$d 个数字" "编辑头像" + "完整地址为%1$s" "加密详情" + "展开消息文本框" "隐藏密码" "加入通话" "跳转到底部" "将地图移动到我的位置" "仅提及" "通知已关闭" + "新提及" + "新消息" "正在进行的通话" "其他用户的头像" "第 %1$d 页" @@ -31,10 +37,13 @@ "%1$s 已读" "点击以显示全部" "撤回反应 %1$s" + "移除表情符号%1$s" "房间头像" "发送文件" + "限时操作,您有一分钟的时间来验证" "显示密码" "开始通话" + "墓碑聊天室" "用户头像" "用户菜单" "查看头像" @@ -79,6 +88,7 @@ "启用" "结束投票" "输入 PIN" + "完成" "忘记密码?" "转发" "返回" @@ -101,6 +111,7 @@ "否" "以后再说" "好" + "打开上下文菜单" "打开设置" "用其他方式打开" "置顶" @@ -110,7 +121,7 @@ "拒绝" "移除" "删除标题" - "删除消息" + "移除消息" "回复" "在消息列中回复" "举报" @@ -125,7 +136,9 @@ "保存" "搜索" "发送" + "发送编辑后的消息" "发送消息" + "发送语音消息" "分享" "分享链接" "显示" @@ -150,9 +163,14 @@ "有可用升级" "关于" "可接受的使用政策" + "添加账户" + "添加另一个账户" "添加标题" "高级设置" + "一张图片" "分析" + "你离开了聊天室" + "您已被注销当前会话" "外观" "音频" "已屏蔽用户" @@ -167,6 +185,7 @@ "邀请已拒绝" "深色" "解密错误" + "描述" "开发者选项" "设备 ID" "私聊" @@ -239,9 +258,12 @@ "%d 票" + "正在准备…" "隐私政策" "私有聊天室" + "私有空间" "公共聊天室" + "公开空间" "回应" "回应" "理由" @@ -258,6 +280,9 @@ "聊天室" "聊天室名称" "例如:您的项目名称" + + "%1$d 房间" + "保存的更改" "正在保存" "屏幕锁定" @@ -265,10 +290,12 @@ "搜索结果" "安全" "已读" + "选择账户" "发送至" "正在发送…" "发送失败" "已发送" + "。 " "服务器不支持" "服务器 URL" "设置" @@ -276,6 +303,10 @@ "正在登出" "发生了一些错误" "我们遇到了一个问题。请重试。" + "空间" + + "%1$d 空间" + "开始聊天…" "贴纸" "成功" @@ -290,7 +321,7 @@ "无法解密" "从不安全的设备发送" "无权访问此消息" - "发送者的已验证身份已改变" + "发送者的已验证身份已重置" "无法向部分用户发送邀请。" "无法发送邀请" "解锁" @@ -306,20 +337,30 @@ "验证身份" "验证用户" "视频" + "高质量" + "质量最好但文件较大" + "低质量" + "最快的上传速度和最小的文件大小" + "标准质量" + "质量与上传速度的平衡" "语音消息" "等待…" "正在等待解密密钥" "您" - "%1$s 的身份似乎已经改变。%2$s" - "%1$s 的 %2$s 身份似乎已经改变。%3$s" + "%1$s的身份已重置。%2$s" + "%1$s %2$s 的身份已重置。%3$s" "(%1$s)" "%1$s 的身份已重置。" - "%1$s 的 %2$s 已验证身份已发生改变。%3$s" + "%1$s %2$s 的身份已重置。%3$s" "撤回验证" "链接 %1$s 将跳转至外部网站 %2$s 确定要继续吗?" "请再次确认链接" + "选择您上传的视频的默认质量。" + "视频上传质量" + "允许的最大文件大小为:%1$s" + "文件太大,无法上传" "已举报房间" "举报并离开房间" "确认" @@ -328,6 +369,11 @@ "警告" "更改尚未保存,确定要返回吗?" "保存更改?" + "允许的最大文件大小为:%1$s" + "选择您要上传的视频的质量。" + "选择视频上传质量" + "搜索表情符号" + "您已在此设备以%1$s 身份登录。" "您的服务器需要升级,以支持 Matrix 鉴权服务和账户创建。" "创建固定链接失败" "%1$s 无法加载地图,请稍后再试。" @@ -344,6 +390,7 @@ "不允许使用某些字符。仅支持字母、数字和以下符号 $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "某些信息尚未发送" "抱歉,发生了错误" + "事件发送者与发送设备的所有者不匹配。" "此加密消息的真实性无法在此设备上保证。" "由先前验证过的用户加密。" "未加密。" @@ -354,6 +401,10 @@ "嗨!请通过 %1$s 与我联系:%2$s" "%1$s Android" "摇一摇以报错" + "屏幕截图" + "%1$s:%2$s" + "选项" + "移除%1$s" "设置" "选择媒体失败,请重试。" "按下消息并选择 “%1$s” 将其包含在此处。" @@ -366,14 +417,19 @@ "无法确认?请前往您的帐户重置您的身份。" "撤回验证并发送" "您可以撤回验证并仍然发送此消息;也可以暂时取消验证,在重新验证 %1$s 后重试。" - "您的消息未发送,因为 %1$s 的已验证身份已发生改变" + "您的消息未发送,因为%1$s的已验证身份已被重置" "仍然发送消息" "%1$s 正在使用一个或多个未经验证的设备。您还是可以继续发送信息;也可以暂时取消,等 %2$s 验证了所有设备后重试。" "您的消息未发送,因为%1$s尚未验证所有设备" "您有未验证的设备。您仍然可以发送消息;也可以暂时取消,并在验证所有设备后稍后重试。" "您的消息未发送,因为您有尚未验证的设备。" + "编辑管理员或所有者" "处理要上传的媒体失败,请重试。" "无法获取用户信息" + "%1$s 中的消息" + "展开" + "折叠" + "已经在此房间了!" "%1$s / %2$s" "置顶消息 %1$s" "正在加载消息…" @@ -385,7 +441,10 @@ "在 Google Maps 中打开" "在 OpenStreetMap 中打开" "分享这个位置" - "消息未发送,因为 %1$s 的已验证身份已经发生改变。" + "您创建或加入的空间。" + "%1$s • %2$s" + "空间" + "消息未发送,因为%1$s的已验证身份已被重置。" "消息未发送,因为%1$s尚未验证所有设备。" "消息未发送,因为您有尚未验证的设备。" "位置" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 70531b5a27..9e3d8f6035 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -2,6 +2,7 @@ "Add reaction: %1$s" "Avatar" + "Minimise message text field" "Delete" "%1$d digit entered" @@ -10,6 +11,7 @@ "Edit avatar" "The full address will be %1$s" "Encryption details" + "Expand message text field" "Hide password" "Join call" "Jump to bottom" @@ -40,9 +42,9 @@ "Remove reaction with %1$s" "Room avatar" "Send files" + "Time limited action required, you have one minute to verify" "Show password" "Start a call" - "Time limited action required" "Tombstoned room" "User avatar" "User menu" @@ -103,6 +105,7 @@ "Leave" "Leave conversation" "Leave room" + "Leave space" "Load more" "Manage account" "Manage devices" @@ -163,6 +166,8 @@ "Upgrade available" "About" "Acceptable use policy" + "Add an account" + "Add another account" "Adding caption" "Advanced settings" "an image" @@ -180,9 +185,12 @@ "Creating room…" "Request canceled" "Left room" + "Left space" "Invite declined" "Dark" "Decryption error" + "Description" + "Deselect all" "Developer options" "Device ID" "Direct chat" @@ -239,6 +247,7 @@ Reason: %1$s." "%1$s (%2$s)" "No results" "No room name" + "No space name" "Not encrypted" "Offline" "Open source licenses" @@ -292,14 +301,18 @@ Reason: %1$s." "Search results" "Security" "Seen by" + "Select an account" + "Select all" "Send to" "Sending…" "Sending failed" "Sent" ". " "Server not supported" + "Server unreachable" "Server URL" "Settings" + "Share space" "Shared location" "Signing out" "Something went wrong" @@ -374,6 +387,8 @@ Are you sure you want to continue?" "The max file size allowed is: %1$s" "Select the quality of the video you want to upload." "Select video upload quality" + "Search emojis" + "You\'re already logged in on this device as %1$s." "Your homeserver needs to be upgraded to support Matrix Authentication Service and account creation." "Failed creating the permalink" "%1$s could not load the map. Please try again later." @@ -444,6 +459,7 @@ Are you sure you want to continue?" "Share this location" "Spaces you have created or joined." "%1$s • %2$s" + "%1$s space" "Spaces" "Message not sent because %1$s’s verified identity was reset." "Message not sent because %1$s has not verified all devices." diff --git a/libraries/ui-utils/build.gradle.kts b/libraries/ui-utils/build.gradle.kts index 62962fbcb4..95ce3d21a1 100644 --- a/libraries/ui-utils/build.gradle.kts +++ b/libraries/ui-utils/build.gradle.kts @@ -1,3 +1,5 @@ +import extension.testCommonDependencies + /* * Copyright 2023, 2024 New Vector Ltd. * @@ -16,9 +18,6 @@ android { implementation(projects.libraries.androidutils) implementation(projects.services.toolbox.impl) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.truth) + testCommonDependencies(libs) } } diff --git a/libraries/usersearch/impl/build.gradle.kts b/libraries/usersearch/impl/build.gradle.kts index d720eef9fa..c7378aa07a 100644 --- a/libraries/usersearch/impl/build.gradle.kts +++ b/libraries/usersearch/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -15,7 +16,7 @@ android { namespace = "io.element.android.libraries.usersearch.impl" } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.core) @@ -26,11 +27,7 @@ dependencies { api(projects.libraries.usersearch.api) implementation(libs.kotlinx.collections.immutable) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.usersearch.test) } diff --git a/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSource.kt b/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSource.kt index 9f3b38254a..2d968cfb20 100644 --- a/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSource.kt +++ b/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSource.kt @@ -7,16 +7,17 @@ package io.element.android.libraries.usersearch.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.usersearch.api.UserListDataSource -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class MatrixUserListDataSource @Inject constructor( +@Inject +class MatrixUserListDataSource( private val client: MatrixClient ) : UserListDataSource { override suspend fun search(query: String, count: Long): List { diff --git a/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt b/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt index 51182af23b..c486c898e1 100644 --- a/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt +++ b/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt @@ -7,7 +7,8 @@ package io.element.android.libraries.usersearch.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.MatrixPatterns @@ -20,10 +21,10 @@ import io.element.android.libraries.usersearch.api.UserSearchResultState import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class MatrixUserRepository @Inject constructor( +@Inject +class MatrixUserRepository( private val client: MatrixClient, private val dataSource: UserListDataSource ) : UserRepository { diff --git a/libraries/voiceplayer/api/build.gradle.kts b/libraries/voiceplayer/api/build.gradle.kts index 82876b4734..4490dbbe11 100644 --- a/libraries/voiceplayer/api/build.gradle.kts +++ b/libraries/voiceplayer/api/build.gradle.kts @@ -1,5 +1,3 @@ -import extension.setupAnvil - /* * Copyright 2024 New Vector Ltd. * @@ -14,8 +12,6 @@ android { namespace = "io.element.android.libraries.voiceplayer.api" } -setupAnvil() - dependencies { implementation(libs.androidx.annotationjvm) implementation(libs.coroutines.core) diff --git a/libraries/voiceplayer/impl/build.gradle.kts b/libraries/voiceplayer/impl/build.gradle.kts index f579586aa0..053914d86b 100644 --- a/libraries/voiceplayer/impl/build.gradle.kts +++ b/libraries/voiceplayer/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2024 New Vector Ltd. @@ -14,7 +15,7 @@ android { namespace = "io.element.android.libraries.voiceplayer.impl" } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.libraries.voiceplayer.api) @@ -30,15 +31,9 @@ dependencies { implementation(libs.androidx.annotationjvm) implementation(libs.coroutines.core) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) - testImplementation(libs.test.mockk) - testImplementation(libs.test.turbine) + testCommonDependencies(libs) testImplementation(libs.coroutines.core) - testImplementation(libs.coroutines.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.mediaplayer.test) testImplementation(projects.services.analytics.test) - testImplementation(projects.tests.testutils) } 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 e6d596e90a..f109f3d49a 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 @@ -7,7 +7,8 @@ package io.element.android.libraries.voiceplayer.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.annotations.SessionCoroutineScope @@ -17,11 +18,11 @@ import io.element.android.libraries.voiceplayer.api.VoiceMessagePresenterFactory import io.element.android.libraries.voiceplayer.api.VoiceMessageState import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineScope -import javax.inject.Inject import kotlin.time.Duration @ContributesBinding(RoomScope::class) -class DefaultVoiceMessagePresenterFactory @Inject constructor( +@Inject +class DefaultVoiceMessagePresenterFactory( private val analyticsService: AnalyticsService, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, 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 9f000b8c32..a1f4de2087 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 @@ -7,10 +7,10 @@ package io.element.android.libraries.voiceplayer.impl -import com.squareup.anvil.annotations.ContributesBinding -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.di.RoomScope @@ -56,7 +56,8 @@ interface VoiceMessageMediaRepo { suspend fun getMediaFile(): Result } -class DefaultVoiceMessageMediaRepo @AssistedInject constructor( +@AssistedInject +class DefaultVoiceMessageMediaRepo( @CacheDirectory private val cacheDir: File, mxcTools: MxcTools, private val matrixMediaLoader: MatrixMediaLoader, 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 e57f065f06..64a479105d 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 @@ -7,7 +7,8 @@ package io.element.android.libraries.voiceplayer.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.RoomScope @@ -20,7 +21,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.update import java.io.File -import javax.inject.Inject /** * A media player specialized in playing a single voice message. @@ -116,8 +116,9 @@ class DefaultVoiceMessagePlayer( mimeType: String?, filename: String?, ) : VoiceMessagePlayer { - @ContributesBinding(RoomScope::class) // Scoped types can't use @AssistedInject. - class Factory @Inject constructor( + @ContributesBinding(RoomScope::class) // Scoped types can't use @Inject. + @Inject +class Factory( private val mediaPlayer: MediaPlayer, private val voiceMessageMediaRepoFactory: VoiceMessageMediaRepo.Factory, ) : VoiceMessagePlayer.Factory { diff --git a/libraries/voicerecorder/api/build.gradle.kts b/libraries/voicerecorder/api/build.gradle.kts index eb296a52bf..b2be9d5aba 100644 --- a/libraries/voicerecorder/api/build.gradle.kts +++ b/libraries/voicerecorder/api/build.gradle.kts @@ -1,5 +1,3 @@ -import extension.setupAnvil - /* * Copyright 2023, 2024 New Vector Ltd. * @@ -14,8 +12,6 @@ android { namespace = "io.element.android.libraries.voicerecorder.api" } -setupAnvil() - dependencies { implementation(libs.androidx.annotationjvm) implementation(libs.coroutines.core) diff --git a/libraries/voicerecorder/impl/build.gradle.kts b/libraries/voicerecorder/impl/build.gradle.kts index bd3f7e72cb..a09106a8c3 100644 --- a/libraries/voicerecorder/impl/build.gradle.kts +++ b/libraries/voicerecorder/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -14,13 +15,12 @@ android { namespace = "io.element.android.libraries.voicerecorder.impl" } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.libraries.voicerecorder.api) api(libs.opusencoder) - implementation(libs.dagger) implementation(projects.appconfig) implementation(projects.libraries.matrix.api) implementation(projects.libraries.core) @@ -29,11 +29,6 @@ dependencies { implementation(libs.androidx.annotationjvm) implementation(libs.coroutines.core) - testImplementation(projects.tests.testutils) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) - testImplementation(libs.test.mockk) - testImplementation(libs.test.turbine) + testCommonDependencies(libs) testImplementation(libs.coroutines.core) - testImplementation(libs.coroutines.test) } 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 5e690ab1f7..624282edef 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 @@ -9,12 +9,13 @@ package io.element.android.libraries.voicerecorder.impl import android.Manifest import androidx.annotation.RequiresPermission -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.appconfig.VoiceMessageConfig 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 @@ -37,13 +38,13 @@ import kotlinx.coroutines.yield import timber.log.Timber import java.io.File import java.util.UUID -import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlin.time.TimeSource @SingleIn(RoomScope::class) @ContributesBinding(RoomScope::class) -class DefaultVoiceRecorder @Inject constructor( +@Inject +class DefaultVoiceRecorder( private val dispatchers: CoroutineDispatchers, private val timeSource: TimeSource, private val audioReaderFactory: AudioReader.Factory, diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AndroidAudioReader.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AndroidAudioReader.kt index 5c51217758..cc8a7bbe29 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AndroidAudioReader.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AndroidAudioReader.kt @@ -12,7 +12,7 @@ import android.media.AudioRecord import android.media.audiofx.AutomaticGainControl import android.media.audiofx.NoiseSuppressor import androidx.annotation.RequiresPermission -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.di.RoomScope diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DBovAudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DBovAudioLevelCalculator.kt index 242fd84b88..6001e74615 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DBovAudioLevelCalculator.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DBovAudioLevelCalculator.kt @@ -7,9 +7,9 @@ package io.element.android.libraries.voicerecorder.impl.audio -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.di.RoomScope -import javax.inject.Inject import kotlin.math.log10 import kotlin.math.sqrt @@ -20,7 +20,8 @@ import kotlin.math.sqrt * See: https://en.wikipedia.org/wiki/DBFS */ @ContributesBinding(RoomScope::class) -class DBovAudioLevelCalculator @Inject constructor() : AudioLevelCalculator { +@Inject +class DBovAudioLevelCalculator : AudioLevelCalculator { override fun calculateAudioLevel(buffer: ShortArray): Float { return buffer.rms().dBov().normalize().coerceIn(0f, 1f) } 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 a64d273379..ef02a45160 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 @@ -7,19 +7,20 @@ package io.element.android.libraries.voicerecorder.impl.audio -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.Provider import io.element.android.libraries.di.RoomScope import io.element.android.opusencoder.OggOpusEncoder import timber.log.Timber import java.io.File -import javax.inject.Inject -import javax.inject.Provider /** * Safe wrapper for OggOpusEncoder. */ @ContributesBinding(RoomScope::class) -class DefaultEncoder @Inject constructor( +@Inject +class DefaultEncoder( private val encoderProvider: Provider, config: AudioConfig, ) : Encoder { @@ -31,7 +32,7 @@ class DefaultEncoder @Inject constructor( file: File, ) { encoder?.release() - encoder = encoderProvider.get().apply { + encoder = encoderProvider().apply { init(file.absolutePath, sampleRate) setBitrate(bitRate) // TODO check encoder application: 2048 (voice, default is typically 2049 as audio) diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt index 6b459111a8..917ef14415 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt @@ -9,9 +9,9 @@ package io.element.android.libraries.voicerecorder.impl.di import android.media.AudioFormat import android.media.MediaRecorder -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.RoomScope import io.element.android.libraries.voicerecorder.impl.audio.AudioConfig @@ -19,7 +19,7 @@ import io.element.android.libraries.voicerecorder.impl.audio.SampleRate import io.element.android.libraries.voicerecorder.impl.file.VoiceFileConfig import io.element.android.opusencoder.OggOpusEncoder -@Module +@BindingContainer @ContributesTo(RoomScope::class) object VoiceRecorderModule { @Provides @@ -39,7 +39,7 @@ object VoiceRecorderModule { } @Provides - fun provideVoiceFileConfig(): VoiceFileConfig = + public fun provideVoiceFileConfig(): VoiceFileConfig = VoiceFileConfig( cacheSubdir = "voice_recordings", fileExt = "ogg", 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 8dc6bf2dbe..768233cfe8 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 @@ -7,7 +7,8 @@ package io.element.android.libraries.voicerecorder.impl.file -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.hash.md5 import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.di.RoomScope @@ -15,10 +16,10 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.BaseRoom import java.io.File import java.util.UUID -import javax.inject.Inject @ContributesBinding(RoomScope::class) -class DefaultVoiceFileManager @Inject constructor( +@Inject +class DefaultVoiceFileManager( @CacheDirectory private val cacheDir: File, private val config: VoiceFileConfig, room: BaseRoom, diff --git a/libraries/wellknown/impl/build.gradle.kts b/libraries/wellknown/impl/build.gradle.kts index 3bc23953a0..4de8ecc0fd 100644 --- a/libraries/wellknown/impl/build.gradle.kts +++ b/libraries/wellknown/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2025 New Vector Ltd. @@ -17,7 +18,7 @@ android { namespace = "io.element.android.libraries.wellknown.impl" } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.libraries.wellknown.api) @@ -31,12 +32,8 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.network) - testImplementation(projects.tests.testutils) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) - testImplementation(libs.test.robolectric) + testCommonDependencies(libs) testImplementation(libs.coroutines.core) - testImplementation(libs.coroutines.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.toolbox.test) } diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt index 0c948af1d9..81bbab5ab8 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt @@ -7,7 +7,8 @@ package io.element.android.libraries.wellknown.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient @@ -16,10 +17,10 @@ import io.element.android.libraries.wellknown.api.SessionWellknownRetriever import io.element.android.libraries.wellknown.api.WellKnown import kotlinx.serialization.json.Json import timber.log.Timber -import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultSessionWellknownRetriever @Inject constructor( +@Inject +class DefaultSessionWellknownRetriever( private val matrixClient: MatrixClient, private val parser: Json, ) : SessionWellknownRetriever { diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt index 5d67cd32f0..aa0e28e85a 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt @@ -7,18 +7,19 @@ package io.element.android.libraries.wellknown.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.uri.ensureProtocol -import io.element.android.libraries.di.AppScope import io.element.android.libraries.network.RetrofitFactory import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.WellKnown import io.element.android.libraries.wellknown.api.WellknownRetriever import timber.log.Timber -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultWellknownRetriever @Inject constructor( +@Inject +class DefaultWellknownRetriever( private val retrofitFactory: RetrofitFactory, ) : WellknownRetriever { override suspend fun getWellKnown(baseUrl: String): WellKnown? { diff --git a/plugins/build.gradle.kts b/plugins/build.gradle.kts index f37ef54e44..e40d1f5bbd 100644 --- a/plugins/build.gradle.kts +++ b/plugins/build.gradle.kts @@ -22,7 +22,7 @@ dependencies { implementation(libs.firebase.appdistribution.gradle) implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) implementation(libs.autonomousapps.dependencyanalysis.plugin) - implementation(libs.anvil.gradle.plugin) + implementation(libs.metro.gradle.plugin) implementation(libs.ksp.gradle.plugin) implementation(libs.compose.compiler.plugin) } diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 9cfd2a4a3d..96a308f93f 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -13,9 +13,9 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion * Max versionCode allowed by the PlayStore (for information): * 2_100_000_000 * - * Also note that the versionCode is multiplied by 10 in app/build.gradle.kts#L168: + * Also note that the versionCode is multiplied by 10 in app/build.gradle.kts: * ``` - * output.versionCode.set((output.versionCode.get() ?: 0) * 10 + abiCode)) + * output.versionCode.set((output.versionCode.orNull ?: 0) * 10 + abiCode) * ``` * We are using a CalVer-like approach to version the application. The version code is calculated as follows: * - 2 digits for the year @@ -28,27 +28,80 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion * - the version code: 20250100a (202_501_00a) where `a` stands for the architecture code */ +/** + * Year of the version on 2 digits. + * Do not update this value. it is updated by the release script. + */ private const val versionYear = 25 -private const val versionMonth = 8 -// Note: must be in [0,99] -private const val versionReleaseNumber = 3 +/** + * Month of the version on 2 digits. Value must be in [1,12]. + * Do not update this value. it is updated by the release script. + */ +private const val versionMonth = 9 + +/** + * Release number in the month. Value must be in [0,99]. + * Do not update this value. it is updated by the release script. + */ +private const val versionReleaseNumber = 2 object Versions { + /** + * Base version code that will be set in the Android Manifest. + * The value will be modified at build time to add the ABI code when APK are build. + * AAB will have a ABI code of 0. + * See comment above for the calculation method. + */ const val VERSION_CODE = (2000 + versionYear) * 10_000 + versionMonth * 100 + versionReleaseNumber val VERSION_NAME = "$versionYear.${versionMonth.toString().padStart(2, '0')}.$versionReleaseNumber" - // When updating COMPILE_SDK, please do not forget to update the value for `buildToolsVersion` - // in the file `tools/release/release.sh` + /** + * Compile SDK version. Must be updated when a new Android version is released. + * When updating COMPILE_SDK, please also update BUILD_TOOLS_VERSION. + */ const val COMPILE_SDK = 36 + + /** + * Build tools version. Must be kept in sync with COMPILE_SDK. + * The value is used by the release script. + */ + @Suppress("unused") + private const val BUILD_TOOLS_VERSION = "36.0.0" + + /** + * Target SDK version. Should be kept up to date with COMPILE_SDK. + */ const val TARGET_SDK = 36 - // When updating the `minSdk`, make sure to update the value of `minSdkVersion` in the file `tools/release/release.sh` + /** + * Minimum SDK version for FOSS builds. + */ private const val MIN_SDK_FOSS = 24 + + /** + * Minimum SDK version for Enterprise builds. + */ private const val MIN_SDK_ENTERPRISE = 33 + + /** + * minSdkVersion that will be set in the Android Manifest. + */ val minSdk = if (isEnterpriseBuild) MIN_SDK_ENTERPRISE else MIN_SDK_FOSS + /** + * Java version used for compilation. + * Update this value when you want to use a newer Java version. + */ private const val JAVA_VERSION = 21 + val javaVersion: JavaVersion = JavaVersion.toVersion(JAVA_VERSION) val javaLanguageVersion: JavaLanguageVersion = JavaLanguageVersion.of(JAVA_VERSION) + + // Perform some checks on the values to avoid releasing with bad values + init { + require(versionMonth in 1..12) { "versionMonth must be in [1,12]" } + require(versionReleaseNumber in 0..99) { "versionReleaseNumber must be in [0,99]" } + require(BUILD_TOOLS_VERSION.startsWith(COMPILE_SDK.toString())) { "When updating COMPILE_SDK, please also update BUILD_TOOLS_VERSION" } + } } diff --git a/plugins/src/main/kotlin/extension/AnvilExtensions.kt b/plugins/src/main/kotlin/extension/AnvilExtensions.kt deleted file mode 100644 index 38dc89a4c2..0000000000 --- a/plugins/src/main/kotlin/extension/AnvilExtensions.kt +++ /dev/null @@ -1,76 +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 extension - -import com.squareup.anvil.plugin.AnvilExtension -import org.gradle.accessors.dm.LibrariesForLibs -import org.gradle.api.Project -import org.gradle.api.provider.Provider -import org.gradle.kotlin.dsl.the -import org.gradle.plugin.use.PluginDependency - -/** - * Setup Anvil plugin with the given configuration. - * @param generateDaggerCode whether to enable general Dagger code generation using Kapt. `false` by default. - * @param generateDaggerFactoriesUsingAnvil whether to generate Dagger factories using Anvil instead of Kapt. `true` by default. - * @param componentMergingStrategy how to perform component merging. This is `ComponentMergingStrategy.NONE` by default, which will prevent component merging - * from running. - */ -fun Project.setupAnvil( - generateDaggerCode: Boolean = false, - generateDaggerFactoriesUsingAnvil: Boolean = true, - componentMergingStrategy: ComponentMergingStrategy = ComponentMergingStrategy.NONE, -) { - val libs = the() - - // Add dagger dependency, needed for generated code - dependencies.implementation(libs.dagger) - - // Apply Anvil plugin and configure it - applyPluginIfNeeded(libs.plugins.anvil) - - project.pluginManager.withPlugin(libs.plugins.anvil.get().pluginId) { - // Setup extension - extensions.configure(AnvilExtension::class.java) { - this.generateDaggerFactories.set(generateDaggerFactoriesUsingAnvil) - this.disableComponentMerging.set(componentMergingStrategy == ComponentMergingStrategy.NONE) - - useKsp( - contributesAndFactoryGeneration = true, - componentMerging = componentMergingStrategy == ComponentMergingStrategy.KSP, - ) - } - } - - if (generateDaggerCode) { - // Needed at the top level since dagger code should be generated at a single point for performance reasons - dependencies.add("ksp", libs.dagger.compiler) - } - - // These dependencies are only needed for compose library or application modules - if (project.pluginManager.hasPlugin("io.element.android-compose-library") - || project.pluginManager.hasPlugin("io.element.android-compose-application")) { - // Annotations to generate DI code for Appyx nodes - dependencies.implementation(project.project(":anvilannotations")) - // Code generator for the annotations above - dependencies.add("ksp", project.project(":anvilcodegen")) - } -} - -private fun Project.applyPluginIfNeeded(plugin: Provider) { - val pluginId = plugin.get().pluginId - if (!pluginManager.hasPlugin(pluginId)) { - pluginManager.apply(pluginId) - } -} - -enum class ComponentMergingStrategy { - NONE, - KAPT, - KSP -} diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 0f5afb9690..d46c54e1c3 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -19,6 +19,8 @@ import org.gradle.kotlin.dsl.closureOf import org.gradle.kotlin.dsl.project private fun DependencyHandlerScope.implementation(dependency: Any) = dependencies.add("implementation", dependency) +private fun DependencyHandlerScope.testImplementation(dependency: Any) = dependencies.add("testImplementation", dependency) +private fun DependencyHandlerScope.testReleaseImplementation(dependency: Any) = dependencies.add("testReleaseImplementation", dependency) internal fun DependencyHandler.implementation(dependency: Any) = add("implementation", dependency) // Implementation + config block @@ -32,6 +34,30 @@ private fun DependencyHandlerScope.androidTestImplementation(dependency: Any) = private fun DependencyHandlerScope.debugImplementation(dependency: Any) = dependencies.add("debugImplementation", dependency) private fun DependencyHandlerScope.releaseImplementation(dependency: Any) = dependencies.add("releaseImplementation", dependency) +/** + * Dependencies used for unit tests. + */ +fun DependencyHandlerScope.testCommonDependencies( + libs: LibrariesForLibs, + includeTestComposeView: Boolean = false, +) { + testImplementation(libs.androidx.test.ext.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.appyx.junit) + testImplementation(libs.test.arch.core) + testImplementation(libs.test.junit) + testImplementation(libs.test.mockk) + testImplementation(libs.test.robolectric) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(project(":tests:testutils")) + if (includeTestComposeView) { + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) + } +} + /** * Dependencies used by all the modules */ @@ -59,7 +85,7 @@ fun DependencyHandlerScope.composeDependencies(libs: LibrariesForLibs) { fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:androidutils")) - implementation(project(":libraries:deeplink")) + implementation(project(":libraries:deeplink:impl")) implementation(project(":libraries:designsystem")) implementation(project(":libraries:matrix:impl")) implementation(project(":libraries:matrixui")) @@ -81,6 +107,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:mediaupload:impl")) implementation(project(":libraries:usersearch:impl")) implementation(project(":libraries:textcomposer:impl")) + implementation(project(":libraries:accountselect:impl")) implementation(project(":libraries:roomselect:impl")) implementation(project(":libraries:cryptography:impl")) implementation(project(":libraries:voiceplayer:impl")) diff --git a/plugins/src/main/kotlin/extension/DependencyInjectionExtensions.kt b/plugins/src/main/kotlin/extension/DependencyInjectionExtensions.kt new file mode 100644 index 0000000000..01895c4000 --- /dev/null +++ b/plugins/src/main/kotlin/extension/DependencyInjectionExtensions.kt @@ -0,0 +1,49 @@ +/* + * 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 extension + +import org.gradle.accessors.dm.LibrariesForLibs +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.the +import org.gradle.plugin.use.PluginDependency + +/** + * Setup the Metro plugin with the shared configuration. + * @param generateNodeFactories Whether to set up the KSP plugin and dependencies to generate Appyx Node factories. + */ +fun Project.setupDependencyInjection( + generateNodeFactories: Boolean = shouldApplyAppyxCodegen(), +) { + val libs = the() + + // Apply Metro plugin and configure it + applyPluginIfNeeded(libs.plugins.metro) + + if (generateNodeFactories) { + applyPluginIfNeeded(libs.plugins.ksp) + + // Annotations to generate DI code for Appyx nodes + dependencies.implementation(project.project(":annotations")) + // Code generator for the annotations above + dependencies.add("ksp", project.project(":codegen")) + } +} + +// These dependencies should only be needed for compose library or application modules +private fun Project.shouldApplyAppyxCodegen(): Boolean { + return project.pluginManager.hasPlugin("io.element.android-compose-library") + || project.pluginManager.hasPlugin("io.element.android-compose-application") +} + +private fun Project.applyPluginIfNeeded(plugin: Provider) { + val pluginId = plugin.get().pluginId + if (!pluginManager.hasPlugin(pluginId)) { + pluginManager.apply(pluginId) + } +} diff --git a/plugins/src/main/kotlin/extension/KoverExtension.kt b/plugins/src/main/kotlin/extension/KoverExtension.kt index df927f48d6..04a647321e 100644 --- a/plugins/src/main/kotlin/extension/KoverExtension.kt +++ b/plugins/src/main/kotlin/extension/KoverExtension.kt @@ -32,8 +32,8 @@ val localAarProjects = listOf( val excludedKoverSubProjects = listOf( ":app", - ":anvilannotations", - ":anvilcodegen", + ":annotations", + ":codegen", ":tests:testutils", // Exclude modules which are not Android libraries // See https://github.com/Kotlin/kotlinx-kover/issues/312 @@ -64,23 +64,11 @@ fun Project.setupKover() { excludes { classes( // Exclude generated classes. - "*_ModuleKt", - "anvil.hint.binding.io.element.*", - "anvil.hint.merge.*", - "anvil.hint.multibinding.io.element.*", - "anvil.module.*", + "*_Module", + "*_AssistedFactory", "com.airbnb.android.showkase*", "io.element.android.libraries.designsystem.showkase.*", - "io.element.android.x.di.DaggerAppComponent*", - "*_Factory", - "*_Factory_Impl", - "*_Factory$*", - "*_Module", - "*_Module$*", - "*Module_Provides*", - "Dagger*Component*", "*ComposableSingletons$*", - "*_AssistedFactory_Impl*", "*BuildConfig", // Generated by Showkase "*Ioelementandroid*PreviewKt$*", @@ -92,8 +80,6 @@ fun Project.setupKover() { "*Presenter\$present\$*", // Forked from compose "io.element.android.libraries.designsystem.theme.components.bottomsheet.*", - // Test presenters - "io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter", // Konsist code to make test fails "io.element.android.tests.konsist.failures", ) diff --git a/plugins/src/main/kotlin/extension/locales.kt b/plugins/src/main/kotlin/extension/locales.kt index b123ac4914..93431455a5 100644 --- a/plugins/src/main/kotlin/extension/locales.kt +++ b/plugins/src/main/kotlin/extension/locales.kt @@ -12,6 +12,7 @@ val locales = setOf( "el", "en", "en-rUS", + "eo", "es", "et", "eu", @@ -22,6 +23,7 @@ val locales = setOf( "in", "it", "ka", + "ko", "lt", "nb", "nl", diff --git a/screenshots/de/appnav.loggedin_LoggedInView_Day_2_de.png b/screenshots/de/appnav.loggedin_LoggedInView_Day_2_de.png index 357f34244f..9d5c4e5c4e 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:382aa5e1c79c5d9426b0cda73d0f003959622f3c59c8135949c5aeed9f8e1bd2 -size 43300 +oid sha256:ec276c2ceeeb57006ff4ad5b45e772efe8240adf461297b92ddc1a70d52e1839 +size 38187 diff --git a/screenshots/de/appnav.loggedin_LoggedInView_Day_3_de.png b/screenshots/de/appnav.loggedin_LoggedInView_Day_3_de.png index 39da6c2866..dcedafebdc 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:edcca49ae428332a771ecb4f2f546061bae5781e292dee68ec8a147dc75099eb -size 27583 +oid sha256:f2af17e0ef9011dfa30dfc4fcbdd2a452a6f1d7d36cddaa885cf55d1b5b79d0c +size 26978 diff --git a/screenshots/de/appnav.root_RootView_Day_0_de.png b/screenshots/de/appnav.root_RootView_Day_0_de.png index b4960cb90f..504397d553 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:6c007c2f9fbf109b7516ce98b50cf586959c262e4c947302fb5daed88674d21c -size 26357 +oid sha256:a5443d72058c1ea02780c1ce8a4fb219533336484c4f7a3c40f6be995ef6718a +size 26285 diff --git a/screenshots/de/appnav.root_RootView_Day_1_de.png b/screenshots/de/appnav.root_RootView_Day_1_de.png index 062f6ae240..46bb168a28 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:74176ca9eaefcbf784912ff7acf8d9cc0fa35fd17e26f13f1fe5cb0ab3dac620 -size 30773 +oid sha256:f74575d51272614ab69d9c94e1adfc7a47f6ec73fd26e1d1d21f336f3827f55b +size 30018 diff --git a/screenshots/de/appnav.root_RootView_Day_2_de.png b/screenshots/de/appnav.root_RootView_Day_2_de.png index 456fea227e..51212b80c7 100644 --- a/screenshots/de/appnav.root_RootView_Day_2_de.png +++ b/screenshots/de/appnav.root_RootView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c6b161fcc127ce493ad38d8e7b54ae7c4c588a2fba7c06cbf9a6b8f6a404921 -size 21806 +oid sha256:5cd2f0f141c1195c450c42d0d0af52db289162442527bb54127bb1b23f0c4333 +size 21781 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 6a94289a2e..7537008da7 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:c43719372429a34f27d2e33203598c515b2cd6b5e0fd44957018ac9ab4e1496c -size 25888 +oid sha256:1fb9d862b82c3df8f1459f2059add211178e9a53b4d195a70cdb178909a04fa0 +size 25422 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 035c7b8146..daaa90702b 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:2b0b3de4622987eba0daf2221a2029cf2f1a872065f7c26bc27b067413d806d8 -size 86873 +oid sha256:d6ca3832f5bfd853eb8a46f229269292b41deea505946972f6c181d3f916487c +size 85831 diff --git a/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_1_de.png b/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_1_de.png index dd5402556e..7fac6ab0b0 100644 --- a/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_1_de.png +++ b/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c533e6dd121969ca966e7d084649750c701b4e7e09ba7276095ce5859a39070 -size 84571 +oid sha256:1ae6e7a291307b66aa36e59bb8a0b0e1e06e030d87a21a134e25e60ab1aa78dd +size 87419 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 7cac8ad21d..f63cfd6ee8 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:3703e9876fcbf94befc13f8f7fc91c266fc0dea558f84288c612623d3a1cf63f -size 11101 +oid sha256:4c16876ec7bd2b6823d16f23135ccabbcb0c85b860b467bfce0373e1f3db76cf +size 11095 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 82ac489296..d9d4d8cc4f 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:486f72ab9be5c85ec5710f2b463801066b57225dc3e707416d067385ea7bb31d -size 19033 +oid sha256:ab0c61a6049caddac4880fc8605e169351af36f87cb16dbbeab3c5f7e9575796 +size 19029 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 index 3b27bbcfed..616c9b651c 100644 --- a/screenshots/de/features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_de.png +++ b/screenshots/de/features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7223973e6d10b19d26354c55a5e627fae38db59e979cca13c0bbcde0d00d996a -size 26526 +oid sha256:e959b0af86942bdf6680e3865f2d093644ce3f9a7b56bb7fe84c30c6e1639b27 +size 23454 diff --git a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_10_de.png b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_10_de.png index a6d327e503..0e4b5b1562 100644 --- a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_10_de.png +++ b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e52ee50124dcdbd8e97de68d8baed83d5dd0c144727dcb9010355b54cac4a07 -size 52250 +oid sha256:12634dceaef537d5431771d900e476409d7efd4cca1d278c21ef424d609e0596 +size 53107 diff --git a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_11_de.png b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_11_de.png index 09956e023e..7f1cba04c2 100644 --- a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_11_de.png +++ b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63390a24b2b70cfcc4ea589fc3f1149a120081c2f34e35f67b9cbc60953787de -size 57459 +oid sha256:10ff5140b95d150a0541cc4d05eb415ccceccf82e797af1cd1074ee387def109 +size 55455 diff --git a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_12_de.png b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_12_de.png index 9552d6c29e..a4b949a6da 100644 --- a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_12_de.png +++ b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7bf47c5978b40460cb2459f5bc7c7e55db5493254877f7eac4c4f769d86fd935 -size 57395 +oid sha256:c50792d349bfd0cb583ba51f754102a25475375c498941a0d113581274c2b3a4 +size 55478 diff --git a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_1_de.png b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_1_de.png index f5480fcc72..caee19be3f 100644 --- a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_1_de.png +++ b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:136e4a39b532d867e3ed2bea92f1eebd982d774bb13579a0e4f1423bb8d6fdc8 -size 72276 +oid sha256:dfaee10346f165f2f27723494d1c2506fbd753e6876f98047059346815940ea5 +size 71902 diff --git a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_2_de.png b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_2_de.png index 71e7dc85ca..311b2cd71c 100644 --- a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_2_de.png +++ b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:963b25054a52d6f3c70c48733ee853eccc87a36b3261771fbac78349e47fab76 -size 65917 +oid sha256:c505ae703366d52fa9e9b0c0ef723c9fc33b073b8bd84882f3fa5aa31f976de0 +size 65504 diff --git a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_3_de.png b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_3_de.png index 804dc18d6c..6aa02e515a 100644 --- a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_3_de.png +++ b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a626646341dea6cd58abf2368b8d17baf6824715bda10d56cccc8f4408536e34 -size 65747 +oid sha256:6b466cab816ec224a7434ec143bbc086941afd01a5370f7d0f8126af603a1634 +size 65339 diff --git a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_4_de.png b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_4_de.png index ce4e3862f9..d351bb2ab9 100644 --- a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_4_de.png +++ b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e36570dbe7840c0942f958cc44faad782c9fc9e347ae05bd5aac5e046f158a8 -size 60674 +oid sha256:64ffef7fbc235b64797dfd89f96b75323f345115b63530aba97e1be8e75b9458 +size 59221 diff --git a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_5_de.png b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_5_de.png deleted file mode 100644 index 26582e0da8..0000000000 --- a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_5_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d045a9a6d4a1ba06d708e9875d1246646fed01c811ad61309cda5a994ecc53a7 -size 13955 diff --git a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_6_de.png b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_6_de.png index 49b2e961de..6739289e2e 100644 --- a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_6_de.png +++ b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29d9b9bfc585d104903af4264ae0d4520b4c0b5008f57bcba4852f37f4235025 -size 65210 +oid sha256:c485a47e58864cfa945c609cc53f3b2875623208f25505af4ac9338728e26f81 +size 66039 diff --git a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_7_de.png b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_7_de.png index 1a52c3cbec..f2a53e604e 100644 --- a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_7_de.png +++ b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:539a21e32ccf9e55cdf3241dad400cb64f1e82d29982be671408444c7f078577 -size 67534 +oid sha256:6fce675e4f044f255c306e917d7473bad6a22c244629b9c5729d08ce3ec7dcd7 +size 66276 diff --git a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_8_de.png b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_8_de.png index eb1f49cbde..2b14561f41 100644 --- a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_8_de.png +++ b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66b22e46349cffe7107aa396ff1b39084c75bf7c3e6be319af6d632fa6a58f00 -size 56051 +oid sha256:b822c8431d2e90a1c484b78edf3eda9db70485669dea0638c5f74c286f94cd20 +size 55755 diff --git a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_9_de.png b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_9_de.png index d3e42762da..dfb0b4f039 100644 --- a/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_9_de.png +++ b/screenshots/de/features.changeroommemberroles.impl_ChangeRolesView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3bd61b0932cf192f84348e57a67d5bc6ceaa4f52c3e442f70dbdfc242834ca0 -size 68212 +oid sha256:65594c19b36303f28c6580aa1d0861f65483f91fc69dfa97a9744895a1c910f9 +size 67794 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 740e98aeee..6a97b76076 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:7fdbc90badc8e97594eea8768680d596b70885dfd0d3c0f337c23a29b907333d -size 11730 +oid sha256:3b105f5a773cfc0d6992a311eeb7edb60faaac10d3747f96d094438e3ce62698 +size 12924 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 aa2f33a059..6793f120a6 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:15f307e09074490ae9cdf58ac0007ad746820c10fb31a85eb297b2de4f5d79f6 -size 12031 +oid sha256:4b42a01b666a6f79ca065bc122093fbb8eb34839f614c8b06621ff42e36c5086 +size 13387 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 740e98aeee..6a97b76076 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:7fdbc90badc8e97594eea8768680d596b70885dfd0d3c0f337c23a29b907333d -size 11730 +oid sha256:3b105f5a773cfc0d6992a311eeb7edb60faaac10d3747f96d094438e3ce62698 +size 12924 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 new file mode 100644 index 0000000000..6a97b76076 --- /dev/null +++ b/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b105f5a773cfc0d6992a311eeb7edb60faaac10d3747f96d094438e3ce62698 +size 12924 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 f0be139f19..b44c8aff18 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:bcca6c2f8aaa76470689e28a6fd500ea953c04ca6ef6db6b3303681bcb4a2f43 -size 37279 +oid sha256:c44e406f4576869def5292961e4201f68e9668d17bf79315e313fccacf58bc76 +size 34201 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 6051c305cf..dfc55ad0bf 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:8708ef75fadcee2a3c8fe2eb194a47320ea82595060772ebf58781a73a653880 -size 43183 +oid sha256:ef7d027bb08c6c74716f588ab90b41cbf21c8eadbdc5cfabc8c843a82e6647cc +size 40227 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 f5da20f574..91a7ed7f55 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:6e581f11c0ebaecb0832ce32223a337a2343355b65520149d9af8688d083ae03 -size 64745 +oid sha256:2e3188c26344223653bd7f8aa2b787d3a2fffebc7e4d9bf1c41d8d911dedac90 +size 61456 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 7d3b090309..e255d8edc1 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:b25c9cf19aaa201209aa036cf9f6d7e6b628c3a116396f331e57ec201ddc616c -size 64950 +oid sha256:07c35a8d865a5902ffb0c382ca4c8daa04e20865dddd669e5c28f49f68223ebc +size 62218 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 1207ed4e6c..9d08205d11 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:52112d1039e9e8666edc9cd2a0e69dc0f72731de4f53af62628e76b1d17b26d0 -size 65167 +oid sha256:36cfb0c3a753eb56e10384cd2228074d3303bb213fec06e654daf79d2dbaa8ae +size 63630 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 f5da20f574..91a7ed7f55 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:6e581f11c0ebaecb0832ce32223a337a2343355b65520149d9af8688d083ae03 -size 64745 +oid sha256:2e3188c26344223653bd7f8aa2b787d3a2fffebc7e4d9bf1c41d8d911dedac90 +size 61456 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 ae3408792b..99e09224b5 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:fe0c1edf4a574ca1704ef5796c5176eea878bcd83e1c6add00f663313ede6071 -size 38378 +oid sha256:e0261e3244d154a88d9544821c3ec3517425553d311b06a7a586c225677ae01f +size 35291 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 36200802af..c855175cb1 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:b5367e6e4986442e85fe0dc39f458a287ccddc7a1bf27c4b10c4d33b3a1acbde -size 44663 +oid sha256:7dbbfaa3f7113abc3d7b3d2c6451f7b6c20bf3a28f64d7407682c88576c62300 +size 41590 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 d496c5203c..86dfef35db 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:1d85d1fc7eafff74f8ae66a8eb429a68c3c36c62a6b7239926c345bcea48f8e4 -size 66929 +oid sha256:2cff0e19fb2f6f09a7173fde3612c55cc2cbe3028c5a8c09ac3efc623bd35da1 +size 63502 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 c37a4cf693..4fc3002681 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:28a7945f030c534c8c903d0396f646cd2344fc250bb16fc5568c15eaf856f311 -size 67222 +oid sha256:e4ff4eb745d680311927f55512cc24b04a87b28faf3845ab31b50fcc8749c382 +size 64292 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 7c1abd3c5c..fc4c6cbc45 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:fc16a8c496e99a3c06860eecb55af7537bac2e9f3ac7321463e717cbdf0fb1dd -size 67459 +oid sha256:900c16973cf8d59b6ace320c848e07eb92056be8e4e14b148a852fd46abeaa69 +size 65817 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 d496c5203c..86dfef35db 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:1d85d1fc7eafff74f8ae66a8eb429a68c3c36c62a6b7239926c345bcea48f8e4 -size 66929 +oid sha256:2cff0e19fb2f6f09a7173fde3612c55cc2cbe3028c5a8c09ac3efc623bd35da1 +size 63502 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 f4634fe25d..2aaf45271a 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:54c0fd7cda1214762bf98d8e2bd133e5b2c64f20435bc7eb9cc4782fc0c3d303 -size 71310 +oid sha256:72a797ea50aef8b2de09aaa54e9710a77ed7f696075be904d6b95032ccce44b6 +size 71406 diff --git a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_de.png b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_de.png index 05319060ae..ea50869a7d 100644 --- a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_de.png +++ b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fb3bc6fa868fda1dd50dc47f6e25a54ef36fc298fba54458ff22782594c5ae8 -size 38429 +oid sha256:8cc57222ee6166b3b415315497191cbc2b90ff4a41f11eb0f19fceb7e097a87a +size 36917 diff --git a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_de.png b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_de.png index 91a16a1c4b..e8e553710b 100644 --- a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_de.png +++ b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cdc5833d44b2b7d461795f58d21f4684fe1d6530dd6a8d439dce059089a50b8 -size 31647 +oid sha256:236c8f484745747889248205a566b313b98b35332271cd2c890aaafe2724a30d +size 30272 diff --git a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_de.png b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_de.png index 218966bb11..7a1f14f76c 100644 --- a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_de.png +++ b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a50de980e1452b445d06abe59a39315b164eb588836ed9d893ede04dc5e1c1c6 -size 44537 +oid sha256:2193e2bab199ad3783eb3b4d524c099c35374ede78adf1ed49d1f04e5bd4d132 +size 43031 diff --git a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_de.png b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_de.png index 06de10e475..13be141568 100644 --- a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_de.png +++ b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6754c86f81bbb3938a24ff1651896ea8184b55727e855c92f364385ba3a527a5 -size 38097 +oid sha256:23b34487e8c852788102c9bcd5a9eca498a2c4165678d8ae4ea7c21d3a62bde3 +size 36594 diff --git a/screenshots/de/features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_de.png b/screenshots/de/features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_de.png index 42b749f8a1..be25e57899 100644 --- a/screenshots/de/features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_de.png +++ b/screenshots/de/features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfd51ec0fa293718e576ba0467980f53cf6691979eadab2fc22078e5fddea5a3 -size 35856 +oid sha256:58cb364d4b03cc76ca2244bd59d844c3bf1133231d92c580229dc69bcb70b40f +size 34442 diff --git a/screenshots/de/features.home.impl.components_DefaultRoomListTopBarMultiAccount_Day_0_de.png b/screenshots/de/features.home.impl.components_DefaultRoomListTopBarMultiAccount_Day_0_de.png new file mode 100644 index 0000000000..6cfe101073 --- /dev/null +++ b/screenshots/de/features.home.impl.components_DefaultRoomListTopBarMultiAccount_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:baac8c8251f75553c31d68ab3c56435303fcf918ebdcbe5986f96222112f4b2c +size 26452 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 index 5bdf640197..7693af2408 100644 --- a/screenshots/de/features.home.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_de.png +++ b/screenshots/de/features.home.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5cfc23d6301501da676ba6e8ef22a37ba69b20a7e55f39fbdf1cfbde1615f72 -size 26650 +oid sha256:b6a48914499aafcd7f65c7f0adc9c9660742308317ed136e2f1a72aa9d7aea8d +size 26794 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 index a8a8fc2fd1..c0ee6482c6 100644 --- a/screenshots/de/features.home.impl.components_DefaultRoomListTopBar_Day_0_de.png +++ b/screenshots/de/features.home.impl.components_DefaultRoomListTopBar_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85fc956fdf2566d8ae24022c78ce74d8e2afa3f1a08e4af15913c83c48d0e0c5 -size 26400 +oid sha256:96aa21d1c28dbfbc328a76c2f3fd0df12692b27905ff80274e04d11f8cfab04a +size 26545 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 index 16aed9b5c3..e35da5b224 100644 --- a/screenshots/de/features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_de.png +++ b/screenshots/de/features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fe8f4056e05d8f27b44afe139548a489b2aefc7abfbe1ef3afa4de5a2f826af -size 34919 +oid sha256:00f0ef61028de8ad1c9886987872f1cf3dfe31f80f313a49099ab3017ccbbfd5 +size 33220 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 index 7aab9954e6..4d7cdaa3c9 100644 --- a/screenshots/de/features.home.impl.components_RoomListContentView_Day_1_de.png +++ b/screenshots/de/features.home.impl.components_RoomListContentView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d92a066157b7651cab7148162a53365425235c1201e00c2c33e53cd98c492a4a -size 22219 +oid sha256:a260a49901aa39a9724af31ad291e251d5ef9ed7065389073a5b22953cee37f6 +size 18709 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 index 99e720cb6e..de59c20f97 100644 --- a/screenshots/de/features.home.impl.components_RoomListContentView_Day_3_de.png +++ b/screenshots/de/features.home.impl.components_RoomListContentView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d85df717cbc8380b8e162d6cece486798b3b236f5aaa1ef4df7877cf4bf1d4f -size 22507 +oid sha256:5520723fc98d5c041c5f13c445f8fe5ce456119ddda8c2eb6b5cdac036aeaa3f +size 21532 diff --git a/screenshots/de/features.home.impl.components_RoomListContentView_Day_4_de.png b/screenshots/de/features.home.impl.components_RoomListContentView_Day_4_de.png index d1e7265ca5..c2c5f858d7 100644 --- a/screenshots/de/features.home.impl.components_RoomListContentView_Day_4_de.png +++ b/screenshots/de/features.home.impl.components_RoomListContentView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:439b75d4e217d0aa90b2e1e8aed5e09819ca6b5f156e59bd4c41aad104791c99 -size 55904 +oid sha256:329befa1d030a76d8a772a69cea1296b31b7d2715c0d74553b26a63845a39caf +size 56164 diff --git a/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_2_de.png b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_2_de.png index 2f28c7b1f7..6f4f1fcb7f 100644 --- a/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_2_de.png +++ b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24d6a8ebb5283c02d919262f80b66e64f0dd5aa58ac10ce818a9541a048d9d99 -size 13326 +oid sha256:4d7943f720f44dd358f5b95fd06215998a5c9cf67f85041e44387cc6df8d7215 +size 13365 diff --git a/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_31_de.png b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_31_de.png index 0b72afcd4f..989e55dfc8 100644 --- a/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_31_de.png +++ b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_31_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c95d45fbe203be354552e9393fa6deef641e945b2f6b65bb4198d059ef5f5ea -size 24655 +oid sha256:e7c0db4c1d8ac8e3a82729f1c11b407f5f9e657890fb7c41d7376c8976188695 +size 24690 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 index d8950ece22..349100c47f 100644 --- a/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_34_de.png +++ b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_34_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d8a6ea0197fe52bf5ee0519ef0ce99ef43adaf319acf88babd3b0812d324bb9 -size 15242 +oid sha256:7447fa97dde37796951804ff2e19a9e2fcd83f9ef0acd62d8591fca467a6c59f +size 15918 diff --git a/screenshots/de/features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_de.png b/screenshots/de/features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_de.png index 390ea764a7..101ebd6709 100644 --- a/screenshots/de/features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_de.png +++ b/screenshots/de/features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f265cdea4cf1f88dbf0e3e717cc51be3c88d65e117f0ad6134b2d693fba88783 -size 38027 +oid sha256:4095db1fc771634666d8a741094154f0199057cc4f4826e834fa7979d9695b04 +size 39140 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 index 7618e1ba16..60ffa078a5 100644 --- a/screenshots/de/features.home.impl.filters_RoomListFiltersView_Day_0_de.png +++ b/screenshots/de/features.home.impl.filters_RoomListFiltersView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3dbee0352a4aa54c00ef91dd05fd5b6e8848881317ac1b1e920c297f02b8363b -size 14178 +oid sha256:0faba67716fb88929d38d0148e23612d1a530b3af1824590506c65feee87ca83 +size 14364 diff --git a/screenshots/de/features.home.impl.filters_RoomListFiltersView_Day_1_de.png b/screenshots/de/features.home.impl.filters_RoomListFiltersView_Day_1_de.png index c118900b7f..1316133253 100644 --- a/screenshots/de/features.home.impl.filters_RoomListFiltersView_Day_1_de.png +++ b/screenshots/de/features.home.impl.filters_RoomListFiltersView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e40bcdd26ad8faa2b1d05e37ca94fe6b23977322faf778bb0ac412f4f7b054b -size 12312 +oid sha256:4b59a077729a19c882de527df5d72c82e68d7a4afc0dfe1c29032b93ea7e23ab +size 12342 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 index e0a8383222..2db0410863 100644 --- a/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_de.png +++ b/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4a7c9af13762a4be6384dad5fef5bf482916dcc880cf43292f75e1db90cb063 -size 29654 +oid sha256:0e350c673e87289bbba38c9bc9115d46de034673a351c741d33e162c41eb5b37 +size 29613 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 index f9166d8d6c..f37924b4b0 100644 --- a/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_de.png +++ b/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f78d019130637a513eccdd9d71e81f313cbd8ca163a83f4d80ccacc134a8156b -size 24277 +oid sha256:d368c5876bb66ee82422e0a896660d44f6106a1b81b06c0fe4af6a5475602f94 +size 23644 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 index 374c18e764..6b9d582870 100644 --- a/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_de.png +++ b/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c92e260805afdc368c89590e1a69ce53800495318d4ef76d3c4864eadb3bfe37 -size 24515 +oid sha256:29ce8b1546df7121cf3f28218b6c6d4c91a7888c22c02ebfe1aceb58b35dad2a +size 23844 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 index 6a160d6e76..908db1df3e 100644 --- a/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_de.png +++ b/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:232e03982572af7052e1921bf740f7c7acd78490dcf26929eaee8ec5f4b5f636 -size 26817 +oid sha256:da9a37c55c9f569d29c5b28f6d7d59974296f69e6fb4d40f47f8cd1afc205118 +size 26032 diff --git a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_0_de.png b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_0_de.png new file mode 100644 index 0000000000..b01fd29b6e --- /dev/null +++ b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ee0972a8becd44e2536c65c379413d319bbca7925290a9c01ce365d4c136c71 +size 109740 diff --git a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_1_de.png b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_1_de.png new file mode 100644 index 0000000000..826c8a88f3 --- /dev/null +++ b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7122aaf00230bbbf059ce09d38572f625f8cb96cc2318a35016d8c65d2d45b82 +size 42451 diff --git a/screenshots/de/features.home.impl_HomeView_Day_0_de.png b/screenshots/de/features.home.impl_HomeView_Day_0_de.png index 23f6dfb6ac..5453ea83cc 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_0_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac64312f55c4cf68abacbcd7e4d88b3287ba93428e4cb1ba2812c9e2f77ef2d9 -size 68179 +oid sha256:bd4033b18eacdf206e2b9d39006626c9eac96b3dc57b8a8200454f4b8b544c5e +size 68319 diff --git a/screenshots/de/features.home.impl_HomeView_Day_10_de.png b/screenshots/de/features.home.impl_HomeView_Day_10_de.png index 7bedf1c0d7..eb7a71d593 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_10_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:934b726566ba2fcf64402ba08889594289a7863b1679ca64ad582b77c2065eb3 -size 37780 +oid sha256:159c14c2f3aceed0993cda75af75d53cdcb7fe58b04a4412c768163328f95df5 +size 36852 diff --git a/screenshots/de/features.home.impl_HomeView_Day_13_de.png b/screenshots/de/features.home.impl_HomeView_Day_13_de.png index 3eba4d46f8..2d7a61f711 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_13_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_13_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e6151975c800745287d4a22a029e175cc1a5e88ba955d69309a7ec49d0dadd0 -size 91385 +oid sha256:cdc61baac9b833fab3823e31910df8ef828848d673436c402113da92e4baf615 +size 92898 diff --git a/screenshots/de/features.home.impl_HomeView_Day_14_de.png b/screenshots/de/features.home.impl_HomeView_Day_14_de.png index ea0733cf4e..5d5a764e7b 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_14_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_14_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb7948f3c3cd2b600604350a0a7aefd45c2f43db3fe501aaff516e6951d54b23 -size 90206 +oid sha256:c3c1fc65c761d7fa0b44ce916aee2ce4af5725877620de2f33b6e03b2c54789e +size 90356 diff --git a/screenshots/de/features.home.impl_HomeView_Day_15_de.png b/screenshots/de/features.home.impl_HomeView_Day_15_de.png index 3ec1b3bc0f..040ac63243 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_15_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_15_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e912cfdb4d5fe7a70348ec8946cc3fa2642d1346dfce59aee77fdcebda8f3428 -size 56492 +oid sha256:c360d5a74cb9cdf48066d18fc68ab4f12caca7d4de079fae7a12f7e99fc7e513 +size 54846 diff --git a/screenshots/de/features.home.impl_HomeView_Day_1_de.png b/screenshots/de/features.home.impl_HomeView_Day_1_de.png index 3f08abe17e..bfec21f772 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_1_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:517fe29f7013d077f5eab2b49414d22ab22389f506ee955f5d1322a87c9aca83 -size 69487 +oid sha256:ed1428c748522383c7f5c58cda95df6398a487d826188c53e285aa6ad3e9a16d +size 69704 diff --git a/screenshots/de/features.home.impl_HomeView_Day_2_de.png b/screenshots/de/features.home.impl_HomeView_Day_2_de.png index 23f6dfb6ac..5453ea83cc 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_2_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac64312f55c4cf68abacbcd7e4d88b3287ba93428e4cb1ba2812c9e2f77ef2d9 -size 68179 +oid sha256:bd4033b18eacdf206e2b9d39006626c9eac96b3dc57b8a8200454f4b8b544c5e +size 68319 diff --git a/screenshots/de/features.home.impl_HomeView_Day_3_de.png b/screenshots/de/features.home.impl_HomeView_Day_3_de.png index e4c377b129..c06b6ccbe6 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_3_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f31bf3e458c2b2f7dbc4bb5a05787f6ec6c3d7f6f2291ddaef0105b93c84cf02 -size 62895 +oid sha256:a9632d63be6dae2ccff6463cbc984b871223cab0cf044f10e453d871b6106d94 +size 63047 diff --git a/screenshots/de/features.home.impl_HomeView_Day_4_de.png b/screenshots/de/features.home.impl_HomeView_Day_4_de.png new file mode 100644 index 0000000000..eaa124d391 --- /dev/null +++ b/screenshots/de/features.home.impl_HomeView_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c21f392c15e20ade2ae53c43c97abd7f5e716c29deb9db9be387de3f25e40f59 +size 60291 diff --git a/screenshots/de/features.home.impl_HomeView_Day_5_de.png b/screenshots/de/features.home.impl_HomeView_Day_5_de.png index 23f6dfb6ac..5453ea83cc 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_5_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac64312f55c4cf68abacbcd7e4d88b3287ba93428e4cb1ba2812c9e2f77ef2d9 -size 68179 +oid sha256:bd4033b18eacdf206e2b9d39006626c9eac96b3dc57b8a8200454f4b8b544c5e +size 68319 diff --git a/screenshots/de/features.home.impl_HomeView_Day_6_de.png b/screenshots/de/features.home.impl_HomeView_Day_6_de.png index 3fc6309668..5dfec9dbe3 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_6_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f02c675f1c604d7bc8c8e6aef925b39aa848136c671e5b40587f006941f7ed12 -size 56531 +oid sha256:f12c3c149d1aaaee451bc771accdaf5af6825bd5b2d0203af66aa0138dd8514b +size 55834 diff --git a/screenshots/de/features.home.impl_HomeView_Day_7_de.png b/screenshots/de/features.home.impl_HomeView_Day_7_de.png index 9cb9163d01..b1566559b0 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_7_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35a307b8d77e932ef23a1b9c9fd2ddd6af16133abe8087c83ee674dcc0599f8d -size 55837 +oid sha256:f2faaaec56e952f4e50517073fc023736740b5ea6f50fce6554de3d2ac2b6871 +size 55158 diff --git a/screenshots/de/features.home.impl_HomeView_Day_8_de.png b/screenshots/de/features.home.impl_HomeView_Day_8_de.png index 7e05236426..fb430bd3b2 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_8_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3fc9e4635e3480448831afc02df965b08e4af95f199f54921c2da7979f2395f -size 54056 +oid sha256:a9dd1a22036e65045c84979c6623d4fa8d7548963dc1958434586a30bf3b2687 +size 53369 diff --git a/screenshots/de/features.home.impl_HomeView_Day_9_de.png b/screenshots/de/features.home.impl_HomeView_Day_9_de.png index 64a622c81b..73d2cf5470 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_9_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b8a3b78d861603d7c1011b60a04805ffc3047794a7162000c90a50ce1b9852a -size 89368 +oid sha256:549c7644f0a20617bf8777175858ab08488d74871b8f027b44a34645a28148bc +size 88335 diff --git a/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_de.png b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_de.png index db83d49f68..ab705ccf3c 100644 --- a/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_de.png +++ b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a74340374c80fe92336288cfa44b543bb743736c49494acb7e2ca24ecbccb99c -size 24184 +oid sha256:38c7a4df03549f8d5bf250877fc984e964e798bd90c40219ef10967f95b29c56 +size 24136 diff --git a/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_de.png b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_de.png index 9f44b23ac1..ecd9db7dbf 100644 --- a/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_de.png +++ b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:623c165b9e861733ce2322d4a0830b7067a69b56e66fc9b359d023233eedec66 -size 31667 +oid sha256:c139e49e6fb3c7aa0f94f2e391cf82160b0dec0609a899af1910522d972ed39f +size 31633 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 index 7aee53451d..9a094a7fe8 100644 --- a/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_de.png +++ b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31ceae603343197facb478bf434d0b24beedd53de383b959ce0291679569b8dc -size 21552 +oid sha256:ec22f26734843749178e93b32358edf07e88fb52149380092c3b99f710842993 +size 21669 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 index 2db96167ec..06e1e4d73d 100644 --- a/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_de.png +++ b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e82bc731dc57594070ba9f50f8c2652b3182ebff17048a0a280583090f287eb6 -size 39534 +oid sha256:14d48a5de0ed3ea22231f63e0c9505fa45b0ed49be9711ade65c3e971fdb90af +size 38115 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 index 7cf58fca3e..84308ef475 100644 --- a/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_de.png +++ b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fa8e55b7d34c2a74d8a9c9c9c4cbe100f58c21b624227bb8537ce68d36ab39a -size 44394 +oid sha256:2663031de6e0be83865e6008f898ba06603132a03136c7f5c49959d883581828 +size 43019 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 index a157cf0d16..32ad3592b3 100644 --- a/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_de.png +++ b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:428a787d1a32b90034a33d24be724952177fa588939f092fdd0e4bab7437befd -size 39737 +oid sha256:2e479455f6c45247609af474edf795e40298614aae7b0d91d28829ca47eaab41 +size 38313 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 index fd915108ea..389639d215 100644 --- a/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_de.png +++ b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89c93db9db74dd51b49b387db2a73a60e6426887e767fe41a8f86874014862b6 -size 34735 +oid sha256:0a8c6ea3cb57beacb3ca3ebdfccb0dfc945861a75cedc74e3fd37f7306fd39f2 +size 33609 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 index 7017d7b5db..06c106589a 100644 --- a/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_de.png +++ b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:acb23fd141e991fa6468c987dac47287c72c052aaf323f5dd87177bffb57e4b7 -size 39154 +oid sha256:47ea6d76991c1a9d4dcd017186fc226150fe7f6fb0c3f772d9fe81c655f671fd +size 38622 diff --git a/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_1_de.png b/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_1_de.png index b6a0411889..15bfd49357 100644 --- a/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_1_de.png +++ b/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77e272e5559e55ba01578448c37b7a72de1b58333c429113d161e37c7ead37a2 -size 20840 +oid sha256:8590b5fd61453580add2dc93262f621b1dbefcff45ed9ef1004d0c5606c35dad +size 21730 diff --git a/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_5_de.png b/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_5_de.png index 6635890823..37e477d72d 100644 --- a/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_5_de.png +++ b/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47f194d3351f597cae41b240bb5f13e901af14aa50d83fef476f1d306dc1790f -size 39269 +oid sha256:f289590cc23cf31013b55d95245d076bb37e6907a8b4409b57e0798847522859 +size 39288 diff --git a/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_6_de.png b/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_6_de.png index 901629f2ca..472cd1d139 100644 --- a/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_6_de.png +++ b/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:489298c50b66250f6343d1bc284e91ba93d0d0b68ea6cc0abe5ee2dddecd3e86 -size 38765 +oid sha256:d531da522778d5692a2f15f05aa4d75e412c8afe8e64aadeacd34b53517d4f8f +size 38254 diff --git a/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_7_de.png b/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_7_de.png index 79a4fd2454..eea89a228e 100644 --- a/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_7_de.png +++ b/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f52566bdfc0c355da83b4aabf120d38638c238c19b31548647faae83776a2b1 -size 30410 +oid sha256:361ffdc21a6bbe6d1e4d460c74e9fd8a2058d489f5d0e716a71a6dd970d7d8fa +size 30254 diff --git a/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_9_de.png b/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_9_de.png new file mode 100644 index 0000000000..15bfd49357 --- /dev/null +++ b/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_9_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8590b5fd61453580add2dc93262f621b1dbefcff45ed9ef1004d0c5606c35dad +size 21730 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 0d3f5f4674..e4c4836b25 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:e0053378b8eae0749b247b8e72de053419adc66fb0eeefc4bb3314befcb43cfe -size 45682 +oid sha256:d1a72880ca6b9b514bafc3bce6a5d014d378ab5d9756b96235f335ac98f2e466 +size 47801 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 5a8020b57f..4ae703b23e 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:d2ae8aadc032491f8be5abbc838a17d6277e723c5a8c3863155b9f1aadb7f7a4 -size 43215 +oid sha256:9399942211a13c9d871692dfd1b421fcd923ddde9f5922979ebfff07c0824060 +size 41067 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 d02438d136..5dcd4a2ba3 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:5c769a2008a85b425ca02f43fb3b7440ff0decb203379916152b306e7742d326 -size 44438 +oid sha256:296d74f3a3198f98599fd2c428268f67b4b70e09fdfbeead9f7c7436e9cf8758 +size 42259 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_13_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_13_de.png index 2617dc2887..f38dfd26a6 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_13_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_13_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6995ce4264efab67f0e80d53070e5fffb0995cf3c3d3a807be2595dc2dddb136 -size 33354 +oid sha256:81ae149fcf6f1a6a52bb90618b2bd6b79192221d2decadd3d78f34ac227a0e86 +size 34158 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_14_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_14_de.png index 13534335fc..729a3f01bf 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_14_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_14_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94d357054411ef2988f4626d7d179c24b68429ef3d9de4faada4e4d14ae791d2 -size 33088 +oid sha256:d76a20fe373382a7448d5c57109662c3cd61c804af734c045bbd376f426eb246 +size 34005 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_15_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_15_de.png index b19b711631..0aee4f3cc9 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_15_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_15_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fce73cfcdb2968e43eb4e01d6a29d9d30e408c1982504e32c599f3209bbdd19 -size 40049 +oid sha256:6ae2cf94255bf1af18b1f26cdf9a807c99f16864c7314bb8cea0e8746beae0f9 +size 40305 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_16_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_16_de.png index 876c6c1ef4..f30e79ecde 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_16_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_16_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04f60f947235f1c55a772a285ff5b5ee048504decaaa290253fad07abb4562ac -size 48110 +oid sha256:a48435579f5429e06ea0e37d2f6cf103fd24f0644c5783aff1036e92bebb1b89 +size 50717 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 d27c6c8212..f5a7654ae8 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:48b16fdde979709df69d7536e6ca7143a9193c1fbcc1fc67e722e343800ee520 -size 37756 +oid sha256:70f6433f61899868f9face9243edf297a55037a313fc153e4e30da49310df594 +size 42103 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 872f83fbd4..8ae05bb269 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:1b9ca0d8f15e669716d7f903321ea6d53efdb5a688d82cf8d87ee7c44e9b9e63 -size 39537 +oid sha256:ff34731937690b47928257c9fb1f3ae9523f9bbe9487131631242991eaead618 +size 40667 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 61c2b5fee4..ea06b4788a 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:24aa1cbaa4b23cac156caa59a1427a3587a162b0431b1a1e12795cb26084f291 -size 27834 +oid sha256:bebec23d5558bb042fb7b72308b42ddca1e72ac3206f68aecf75d6a4736415e8 +size 30936 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 6662f386b9..a0152841dc 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:14f5d812c858fbd055b9bc478c883557f6f2246336693aa4545ffc7416fd36bd -size 47731 +oid sha256:c06469589aea49757c6905a87cf2340b472f5cf9d1170c36816248be356b5252 +size 47878 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 ae8855ddaf..5004e1b490 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:42afee461a148eff01e1cbac6c5ea43864ad411e2951c956e20713d63e071cba -size 31962 +oid sha256:64009b68bb47154dfd51bab44a0ed071ec420377ac219c8bf5c5fcd77edf06df +size 35500 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 db8fec3c75..02b18e602d 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:7cdbd64bf415dd7c35f59387f6fc3660c692af704c9dfd8d3cd96c814b3079f4 -size 31656 +oid sha256:1292c78624d48944ba94da85c8e56f2636d059458bbc330298dcca89131c143f +size 34720 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 68c0f56eac..5b96ed77a8 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:30b3eafeb3167a792b9543d7cb1ea033b8add88435b7c7721dd840e8dbc9537d -size 40476 +oid sha256:3d40fe4aa6b06f8b52ec5b4f94549c35613872b3c84843e814e400139c1ba1cd +size 41872 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 4e83319d93..799892693d 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:27398dbb52e4966b6e83838bb6316d62244419988e7b322dc36b3b1a1d821b96 -size 35003 +oid sha256:287dc0a75daeafebb50f79ec327a02eb019d3016b2e52e28613edf9c6e252315 +size 35560 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png index 50a105a8e6..6b40c0088c 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d281e3b9c1c7c3892a051c2d9b3b11e330555ca3da82bb1976c47f89f5006426 -size 35925 +oid sha256:169b5c52dded2b2618fe19e8adb91b02785c5408802646a88a9658e52f0d57a0 +size 46609 diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_de.png index 7406053cf5..afd3136705 100644 --- a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_de.png +++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a600a47c1b8b81066d3dcddb4243890319695588e852ab859ea5f90b77e35bcd -size 32915 +oid sha256:175810c0046dfd28a9273ae6dbb9d4f89dc5bbfe719bf4a9dd4938b7e03d2e29 +size 32154 diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_de.png index adf9432b0b..ed3160dcf1 100644 --- a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_de.png +++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b86fdc4768404633fc1399a0e5eee8a9a900b8320ada7f7d80a30379c146122 -size 38379 +oid sha256:815272dff296fde43ba3cc8aaf6f7c8e763c8434a400ed2a9b09150f24614f1d +size 37721 diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_de.png index 7fe13a5a03..cf38da1b6e 100644 --- a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_de.png +++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dda327a7a03b3f06f3a128179ae055c9217d9c3c8c21aa42f299aa72091eb91c -size 21651 +oid sha256:30743c42e137b63960d18a76df1b488b055b62384e4d984320606ecf0f591e1c +size 21452 diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_de.png index f46c3a743d..bba069deb0 100644 --- a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_de.png +++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f453b9a19524d101e3a0948cf1ef8eb4d6c0919829816377537b5657203130c3 -size 22258 +oid sha256:8f921e04c544b858f02165d15b1bdc48e71fb9572a6717e52e7c7dcd556e5f64 +size 22488 diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_de.png index c3d7da1541..de8a818e1a 100644 --- a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_de.png +++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64b70351e7ee1203210f328b436ed6a8770aa888a2bc4afa83690b4834f03b00 -size 29468 +oid sha256:183f893c3b7ff13533f6130c85b293547dd505ec2ac223ff5cf4c0c1407171b6 +size 28889 diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_de.png index 77ea53cb32..1fadd8530d 100644 --- a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_de.png +++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75725ca2af99a1cbecd387644a669c28b23628abb01d9c36d3b417f032e5dfe3 -size 36501 +oid sha256:ef454c29137f961510691af29ba49066eb25a59655cb565243a74a08b0678d8a +size 35021 diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_de.png index 0161fce927..32dc93c205 100644 --- a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_de.png +++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03e6f8014e4b06cda4e6eab8248ab1c331def3e719662e3d795c4f2a8d3a852d -size 42940 +oid sha256:3c01028c357b014744faa022ea93801a44d7f938b869a4db1c14992ea732665c +size 42277 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 0aa4b6ff42..05644c04d6 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:86e5ed8130170b72b5ceb469971c7557bdc3699f432a201500a0a09e07ee9a21 -size 29413 +oid sha256:c76f2ea8f90917abce153e75fc162cd41856d122bf8958bc7b71bdc8107b167c +size 28133 diff --git a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_5_de.png b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_5_de.png index ff9833e766..f739c629e1 100644 --- a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_5_de.png +++ b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5dcaa75e8efe68c35e2ab72e381926284bb10f0cf0e0e64efbe1775043fd21b9 -size 47158 +oid sha256:cb4b0c5968cf1859485f0a88e836017ce640e9e7ecc5a8db31df0cd51012da99 +size 48479 diff --git a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_7_de.png b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_7_de.png index 454c30ad4f..60d8909107 100644 --- a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_7_de.png +++ b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6aa1d1412f5f0ebbd3dcca0b364ca87f0170596b1d7cf7dbd57675bce104d9af -size 43808 +oid sha256:a7f9908bb1a05de0c4e97e830f9ee543deb29d8664cad5ae5a29a5a6cb5284d1 +size 44367 diff --git a/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_1_de.png b/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_1_de.png index 9b034722e9..7a3be0bb79 100644 --- a/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_1_de.png +++ b/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37f0d170c4c8a30e0571e4d89cc4654e5292bbb3cb5e1b677038da1e17dcf7ae -size 23056 +oid sha256:b515088a6d4a75f0a29b7587a1cc603c3b9041d6db5ef5971a46eb200ae4e9c6 +size 22041 diff --git a/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_2_de.png b/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_2_de.png index 06998c4f2b..35b86c66d7 100644 --- a/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_2_de.png +++ b/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4db1f3840aa3a4492f9bbb1f4c6995fe1b49588077bfe605900b3ac5ae23f28 -size 35635 +oid sha256:ae5dbb9886afaa7c786ba72bd8fc262813f8a7a24a6dd6418fba8da9c4a9f941 +size 33627 diff --git a/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_3_de.png b/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_3_de.png index 1f3ef82896..56500c61d2 100644 --- a/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_3_de.png +++ b/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0370de5bde85f87e81b05e26d34debc1c35e5e5df7975a96cd3f33d5a639c07 -size 40173 +oid sha256:829e9c8ad89722f365b682627fbc2da90091c07230485d1e2f0827ca5aade44d +size 35700 diff --git a/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_4_de.png b/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_4_de.png index 06998c4f2b..35b86c66d7 100644 --- a/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_4_de.png +++ b/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4db1f3840aa3a4492f9bbb1f4c6995fe1b49588077bfe605900b3ac5ae23f28 -size 35635 +oid sha256:ae5dbb9886afaa7c786ba72bd8fc262813f8a7a24a6dd6418fba8da9c4a9f941 +size 33627 diff --git a/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_5_de.png b/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_5_de.png index 971c82fef3..6be223de8c 100644 --- a/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_5_de.png +++ b/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2fd207b37ad64e6a5e63d55a2b0bbe9ab618bb0ccf1e909550562c8c0b04319 -size 32635 +oid sha256:a66922765b357a16336992692aa74ff3d1e58868ab272008032332a66823a2ae +size 37550 diff --git a/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_6_de.png b/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_6_de.png index 974c12499e..da1fa009a6 100644 --- a/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_6_de.png +++ b/screenshots/de/features.leaveroom.impl_LeaveRoomView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d26e3865c632af1ab3fc8ee97b95ff2db78be0c4961cd36e006f2112db14978e -size 10925 +oid sha256:f4c5bf96d84f2800fc8952fe52f2e60ac260cb784025d5f0570349c4f02a5ae4 +size 10893 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 427d1dc7fa..3496a6c996 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:1620bf8172a3149fecd3c2d1d4dadd4e47583f7a526439aebbb214d63006bb5d -size 37678 +oid sha256:4ec028bee93f35e79e812ae3b6319c29bbb49e3f22184db437c9ae05546b9ace +size 38189 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 f4cd907ff0..864d79e548 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:7924fd960811c92797a8928557182269af37906971e177a170a0e6a28045c0f8 -size 34303 +oid sha256:057899a5bd1686949708242e9b96ca798cf2d8710c65347934216f7f99e67f15 +size 34558 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 f6cbe83e40..b8dd2b8613 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:f70500e3e8371bb007bb5cb73636ef9a29fe2f70c66e6073bac9a6eb84515f7f -size 32231 +oid sha256:b7e861fcd4411820176481f0286ef2761d315a4e360d1e11060c0ff81b79441d +size 32748 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 84c16a9d1c..142072f53f 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:f59dbc9fc2848dcfca04fff3e8739554f8dade43f0f4e189699480ee3073fc92 -size 28901 +oid sha256:28ea4c839cdadaac893336a2a4d010049a16318ade2ada6bd47b836813d1549d +size 29133 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 b44b5bcbec..73d3587019 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:0a9c5e6c1d2650e281ef1ab985ed02ff8ed4fbeff4b2a7f0c2ea8316a407d228 -size 33481 +oid sha256:7415217e713fd3f3828942be2fefa87c9f743c484d50ec60ea1c0a04949bfb65 +size 32895 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 b1f125fff7..81d00f84b0 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:4e50acc00c8e2a9be8300cc98a048c01cb07820e2950cf361e052d1544e6b0e6 -size 33711 +oid sha256:417247c7f2485fb6f30cb3182b008d7f7c924c52930ad0db3b1113e5e641803a +size 30799 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 3bf59255c2..c292b6e7c9 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:d35cf53b0fea5d4fe958d2939935be1f8f75b0f915b9d5fb1388b012919266df -size 33300 +oid sha256:8258316042dfcb5f43fdcecf484a0c920a5b5269baf29ba4f00103c89d5308e8 +size 30385 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 309b68f153..3650e95272 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:7362739ba837f7b6f8900f765cf839609ad8c2602b4edf804d34161056b7e6dc -size 35020 +oid sha256:6ccc829c98481c9af5680b23b50dec0c8abd5ba7fa4bb13c2713822438a7d0bc +size 32019 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 b2bb465cc8..e67751b3c7 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:47a40fe775a83ff3e72e0b96d4abdfbef45a89f93bda4daa4e33b59a7728c153 -size 28269 +oid sha256:339e4dc13f6074ca938d6752878b54752f6c9002c92ebf291136079579f85bd5 +size 27776 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 33e9a935e6..a7a98fafb1 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:9a4ed9ad5e6b6cd9bf529ca2778f78a1b6f32211742427d40a7954c26e6191c0 -size 33099 +oid sha256:fa5cf8e3bbd7116f1c8c27ac29d956929ed6a52e7b2b9a0cc425709ed83c3c1f +size 32583 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 6b274e0be9..a41d3d7552 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:ad807e91edf13a0b07a432cef0ec4051270fe6d0665a9da9d81ed3e03d1f4464 -size 22033 +oid sha256:e5b386b0228838abb900fa711998208c05f77dc87f760c43397058fcb3d02c19 +size 22679 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 20af24a209..91bb9f0d72 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:7bd0da7fa58aff069a75e5b68be1fcc81005d4bc2a088496037bb35672501b45 -size 21645 +oid sha256:ee063246ef3d9f34f75806a5b6e772aaa9a60259d9d5fa19b1be5c0a932880f8 +size 22308 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 30958d8388..1a29ac6b2f 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:5734722a11f4f3648989f5662748253a7b9c94813d02b024e977d937e326a07e -size 23664 +oid sha256:dce56693ed4a6473f477e85ff548630bb72a0c1aa5f063d02abbb7c6c151013d +size 23375 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 41ba5aa710..2c73a48c43 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:4bcbccabd59ee16a298777fa7862c86d30a94ced3845f2d9177cfad69b09f42a -size 38281 +oid sha256:8d5c7d1a7aef1ccceaa1837fd3483bf1067158a9edd0fe7a03aac74d313c3874 +size 37439 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 7b4db5cf46..78c17a0adb 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:caa012799d02c55ef4a764909fde43ef20d999d9c624ab05e1796169e8c31d9a -size 19372 +oid sha256:91e32f6186bf16d4880b31b0648b211b9ea228cfc4fc74e0cdfdb6e66d3d32bf +size 20083 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 b65d13fbef..15bfc89256 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:a77b4959da69c013db537d82c1e36a74e40927c0c904014a5dbe21893171bdf4 -size 35100 +oid sha256:9706510016738e2b74b143c97deba9c7cfe3ecf4735b9f033c0cf4d0522005b0 +size 34216 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 29555d4337..8c25e8ea0a 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:b78c16e8889662fcb7ada84d8208144249674654ae1f8e15eefd0ad39e1aa2b7 -size 23076 +oid sha256:3ccaa6e9d59294afe6dbb65c2bb5e207bf81d3f14e71752518df78316434d706 +size 23541 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 e2b59b3335..9e2974f594 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:0cb9b62d66e1d72ac3aae57354b68bd002c52c9fa892552d4e6bc0f6acb7393d -size 24619 +oid sha256:02cbbc16f07b45feeb638195be476483d68dd652462f8b069a6e1492f66d0089 +size 25068 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 493d6a2ecf..019536128b 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:d19160c5b25fd1964119e0d4d1480f9486d906005413a54d812672b637032fe4 -size 37729 +oid sha256:007d4fbd2d36927ac9b27063b08cfbded0085b58bc4e5d9fbf1e4e1f84d2dd62 +size 38394 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 d1b06b3c80..4cf461ffff 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:117ee32c2088660a7d38bf7fe18856e820bdce9b2c12e472d130da2aa2f08a12 -size 38116 +oid sha256:38dbf516ba28c75aa91b45e3e4f4e3afb1a6959038177bbbe5b0650aa42d1fd3 +size 38798 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 530704c821..35c9c8c261 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:04eaf5012571ac2b87d5ae77ff153e4f22cbdea9a538771eccc958808b8c23f9 -size 39376 +oid sha256:e77abcfd3e6c9214efe1ef6fa40cb0dbc19a4eb7da184d1b4084479fe7d0b57e +size 39081 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 a8ea99167c..f12bab73c8 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:178a66853d9c0606b5ae959ff7df1c9da8f914f631db4beb88a905f54a2a2a4b -size 45119 +oid sha256:727b36b17214000ed929f94b6bcbeb0da66278031e1ffee2fdf547201f542f2f +size 44275 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 9b92cfeca4..35649b1c72 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:374d74c059a126f395282ca1d481c722587733f67c67774d412e94c7e5541080 -size 35126 +oid sha256:1be354458cb21172f8d2e93aacc98228a074536f8dc3dce734fa81446f7f9c47 +size 35793 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 129550c238..78da42de5f 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:46b6a1dec955811a5b56b925517d104ee86a46888a93390109d65e1a73795963 -size 42083 +oid sha256:acc9e12a77d389bbcb17595e5d50f7efd99140eed5935f5c90d9ad82693446df +size 41195 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 70d252ad75..7d8f9e851d 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:253882639345fccf69b7265bb5baaf766937f0225d8ca874a624dd486f89b654 -size 31608 +oid sha256:63a908bfa15be2c537c5d1a20d3066fcfca4b5a60256696ad819fb45b59d7a76 +size 32088 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 59a29f5f6e..9f22132175 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:4ba258eb84a9f198d7639c05449ce6b89fdd1c1e7537477bfa5b64ff3c669209 -size 31808 +oid sha256:88c5747df737c777ca3e1fb039e6507489d2db418d46d970f25b7380ff0dfb40 +size 32282 diff --git a/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_1_de.png b/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_1_de.png index b23fe05d44..c856f0e358 100644 --- a/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_1_de.png +++ b/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:628cc87a2c18dd8416ddfca2a7bde83f9254e79031bbfdbbb41663f3ebc6d0b4 -size 14401 +oid sha256:506f037d7cce42ac09efc89bff5d74475a723321af4713ed54a7caf85bfcb297 +size 14382 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 4e54e0d542..52cb399536 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:6f28525d2760ca6b70e8e61d64a43f84f59c709ca3a5fa05fbfafb6a10edc754 -size 31373 +oid sha256:38bd30d45c29b9655f0e07e76c22d46b0e0ab5e3cce718bcdb135141a098332d +size 32396 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 index 3f09465793..a99f7380f6 100644 --- a/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_3_de.png +++ b/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b5104c3f7bcb024b7ae93fa0da6c4f310ed791805fe276cf140028d84843bab -size 16339 +oid sha256:16da7f89ea01252b4805da19a577c17be69890c45a5fd59f0d9cdadaed24be04 +size 16317 diff --git a/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_4_de.png b/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_4_de.png index b6e6100dc2..3170923cb3 100644 --- a/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_4_de.png +++ b/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:407dc6d0ccd17f88ab433ec1e6b722b1ab10b86124e96e7e58b05dac679dcd91 -size 26710 +oid sha256:7b7c028d55a229004e459a4dd6302eeef9edee8cf8281e8d20bec59f33fb4800 +size 27981 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 4e54e0d542..52cb399536 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:6f28525d2760ca6b70e8e61d64a43f84f59c709ca3a5fa05fbfafb6a10edc754 -size 31373 +oid sha256:38bd30d45c29b9655f0e07e76c22d46b0e0ab5e3cce718bcdb135141a098332d +size 32396 diff --git a/screenshots/de/features.login.impl.login_LoginModeView_Day_0_de.png b/screenshots/de/features.login.impl.login_LoginModeView_Day_0_de.png index 7d6baeb92a..298de207db 100644 --- a/screenshots/de/features.login.impl.login_LoginModeView_Day_0_de.png +++ b/screenshots/de/features.login.impl.login_LoginModeView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ddd215ba236b8f5a72953770a7fa5f1603b72d0ad0a48058994584f4fcb9b29a -size 38598 +oid sha256:01bb49eb69c81c2d00318aed7463436f500e7a69679c05003e468459b3563c94 +size 37005 diff --git a/screenshots/de/features.login.impl.login_LoginModeView_Day_1_de.png b/screenshots/de/features.login.impl.login_LoginModeView_Day_1_de.png index 09833de090..97263af58c 100644 --- a/screenshots/de/features.login.impl.login_LoginModeView_Day_1_de.png +++ b/screenshots/de/features.login.impl.login_LoginModeView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7ab2dc5a541481eb5e348772064a23668a481073ae7b97fb069df9b11f9597e -size 10937 +oid sha256:cd7144624be51c4bc8a4d6d59c898fc50e8607f819c4c6c0bfe75100b9cc4c2d +size 10932 diff --git a/screenshots/de/features.login.impl.login_LoginModeView_Day_2_de.png b/screenshots/de/features.login.impl.login_LoginModeView_Day_2_de.png index c020d43208..0aeed1a21f 100644 --- a/screenshots/de/features.login.impl.login_LoginModeView_Day_2_de.png +++ b/screenshots/de/features.login.impl.login_LoginModeView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3b3e3747fe6f49e003b8cf195f1c363c0bacf2cd1588651193a249007bcdf56 -size 26310 +oid sha256:c0b0fccdffca0e394d24d41d295f15239887157f3aa209eced7f2f75f0b914dd +size 27529 diff --git a/screenshots/de/features.login.impl.login_LoginModeView_Day_3_de.png b/screenshots/de/features.login.impl.login_LoginModeView_Day_3_de.png index 7617196945..683bef00b4 100644 --- a/screenshots/de/features.login.impl.login_LoginModeView_Day_3_de.png +++ b/screenshots/de/features.login.impl.login_LoginModeView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21dbf7e2c3c9fef115de119b26ac00872336d402e0680ba951f89f8b0555e2ef -size 15807 +oid sha256:ab6bd357aaa6bfb530e596693f1c1454274784d53363525c5e8b89efd4599481 +size 15786 diff --git a/screenshots/de/features.login.impl.login_LoginModeView_Day_4_de.png b/screenshots/de/features.login.impl.login_LoginModeView_Day_4_de.png index 4e54e0d542..52cb399536 100644 --- a/screenshots/de/features.login.impl.login_LoginModeView_Day_4_de.png +++ b/screenshots/de/features.login.impl.login_LoginModeView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f28525d2760ca6b70e8e61d64a43f84f59c709ca3a5fa05fbfafb6a10edc754 -size 31373 +oid sha256:38bd30d45c29b9655f0e07e76c22d46b0e0ab5e3cce718bcdb135141a098332d +size 32396 diff --git a/screenshots/de/features.login.impl.login_LoginModeView_Day_5_de.png b/screenshots/de/features.login.impl.login_LoginModeView_Day_5_de.png new file mode 100644 index 0000000000..d8ed4858b8 --- /dev/null +++ b/screenshots/de/features.login.impl.login_LoginModeView_Day_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0e1ea6e6a244f7b1329aaa4ef699d059e9fd137c58a8e78c0e5fc43f2d45832 +size 18149 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 1c47aa2444..d1f5a7496b 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:5e02948e18d4cd3d3ad75e8d8862cc334e9ea227c113fdafb2f25896697ee330 -size 39594 +oid sha256:7a9d211cfab821ec51870368af03d20cfbaa30c6a439bbfb27e1ce37c52533fb +size 39101 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 99347d61f8..804bc0e00d 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:5671d06509aa17fd22b7935c3e695f79432ffe85d276f31d603b99007aa98407 -size 40425 +oid sha256:4d0a901c662f5595c50485e40c212b66528f7e1ecf19ce844c8360a6b22d0572 +size 39965 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 f3402b6b87..49eb1c3e50 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:6dc56e3163d7f90fe07a95d44d42e4e88881fa1a4234bbb601b6c34629bde463 -size 41682 +oid sha256:61d0f8e341e76fee533117b9724aec1e299519e7cff827d43b0977e7d6845ab9 +size 41284 diff --git a/screenshots/de/features.login.impl.screens.createaccount_CreateAccountView_Day_3_de.png b/screenshots/de/features.login.impl.screens.createaccount_CreateAccountView_Day_3_de.png index e8d43eb741..61d358271f 100644 --- a/screenshots/de/features.login.impl.screens.createaccount_CreateAccountView_Day_3_de.png +++ b/screenshots/de/features.login.impl.screens.createaccount_CreateAccountView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bfbe1b69a417a3a08dd9864e1bc4f150538b36906bd06e0c571575f64fc7a058 -size 15585 +oid sha256:f55612c2279875c0d128d124097f6ed36378e16bf4d0a2ce27126e3f76b57147 +size 15587 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 8dbad73482..5411eb86e6 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:ff82d0516a960eb67c20c37ccbd0507488fd1c97ce5aad62f5ad31ae3e9fb79a -size 39527 +oid sha256:c5e226015f1d493ec4cbfb1b11d7112cd4826190a60df0f9286a0f9c0f51cfad +size 38621 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 3bd38cbef7..aee1a55cd2 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:4891560f5fb86acd97d3cce4a2c93c98afabffeb4a9bb958fd381ee2e05a6b66 -size 40733 +oid sha256:92864d613ce5a68ed305bd0b1e337ae2b3fd9aa1c3ad1bdf625f062a25e50824 +size 39815 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 851c928a23..8c48a80d25 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:52959d654fd557c72d2d617f6cbd5d99b9db3c2b1b96435a6484603c61951910 -size 30866 +oid sha256:e73952f62b9f4addbf6c66e6929da91539a451660a6617ac53f995534726f52d +size 30406 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_6_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_6_de.png new file mode 100644 index 0000000000..8e4acbb7b3 --- /dev/null +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4615d81aad221081ceaf20ece5cbb2a9b7440b6a9b684997f7a8e249c8de1b0f +size 281201 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_7_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_7_de.png new file mode 100644 index 0000000000..247acb90dc --- /dev/null +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_7_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5dd50e66c81b2cdd21c7d3ebe9fa1ae1c56c43c271a9ffa57a6868b9e19d230e +size 22756 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 4350b9debc..7a4dd6c1b5 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:e092244b6bca0a8157c22356d623f8da4bce3bcce6178ffb418ccc5594219641 -size 39846 +oid sha256:36a937b32ea014cc94dc00b14da82549ab4d8cc2c2de89f5eb490170c87cd5d4 +size 38356 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 ef7e16ba7b..d78df8c9e6 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:270a32e09d104baba159aa7e7af0b653b3692fbfb07364c004746c1a815899f0 -size 35655 +oid sha256:e9bf45b8d4d39d6b5fc70852b4da0b2e2d7db438303ce4471c396449f0317c46 +size 35373 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 bbbe636e32..106d8548d6 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:774dadd9504e60d48f77f62fd486354ebf7c2a0cdb8b686c905c1f3f5d8398d3 -size 37682 +oid sha256:db2df4d775b8b14a54e41e874d93d242d0e72fae3014e5c73b92fde96264e153 +size 37393 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 3eb9827d49..031d4b02f8 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:42d34f62b65e43a1bf68e9294c635ab9bccceb6105032ed219e7777ee0003d9f -size 30067 +oid sha256:8ca8caeac8dc4d9e4e405daa0377aaab0cf3acaa9f3a744c284450e9f63f9deb +size 29400 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 4e4171eeb3..c7b6fe4608 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:8915153e9db3f7b5d83cfb460524d50254f53ec1626f5d3ce9ff19e917cc04b7 -size 21434 +oid sha256:9a9f40df5c43f367b5851000f67a33093eb7d88772a8c0f7e698222aaec816d9 +size 21736 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 3c0a555d06..80c19bbd5e 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:7b1a89b76ca7ee74776e650a45605f2b78780c67d50dc93f84addf2693c9ec86 -size 58514 +oid sha256:405463cb5f8c7a1fa1d05be06a392455644425a65656596c2dc804f1aa003a5a +size 57424 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 b0c7ca3133..1c97360bff 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:a2a2cde841dcba8af9d039808b76cdd904a077b6a42ac417333acf6aad61cac1 -size 50494 +oid sha256:7ae87ae2b64c271d0e43e9b51502eae24e5f3d917f765761e6840f04b3a417b5 +size 50945 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 index 561f54894e..31bcf59ca2 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d13d28d9c18e3942c5a9260ea341eac5d01e3c129c0004cc6633bd9d5c857f08 -size 34376 +oid sha256:464b9722b90078cbc7391592ed372655c9da484b1c7376f8a65587d79ef750fe +size 34092 diff --git a/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_de.png b/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_de.png index 3adae4bd6b..653caec071 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb79545cf57d8c8fbff8fb6dc298e85ac543d7f3c7bd3c06360e570a98e0fcc0 -size 31445 +oid sha256:ce738d099663f704b97259ac841dda2750f98d11670228a5e3b851a409d6499e +size 32198 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 a68f0ee529..36aa350e77 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:cf4bb287246f1d13009dcd1d46c25f51bbd03dbf8926d0ff60eef4c06c3efabd -size 18843 +oid sha256:c6be36ccc68599cc3687c6926c5ed965493c841dd6366047c04f23bea1a6254b +size 18833 diff --git a/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_3_de.png b/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_3_de.png index 460eb8cad4..251b8a0496 100644 --- a/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_3_de.png +++ b/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f87747043f62847839dc9eb80a4011d555d230a1310edeaa2c8bf6395213313 -size 20660 +oid sha256:33601bf38dab409508b12c6f864d5529fdb24d17bd54caf50502abdfe4e04ea5 +size 20636 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 71b05c5f09..6e765c4d2b 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:5dab04b4916a5ed318c4c9e2216e2cbc070cad0df0d30ae29aa93f13efdff3a3 -size 89168 +oid sha256:f80d0892014051edb5e84dfe11c6ee389c7fe168143b209b8272c6426582063b +size 91934 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 e34159f7c7..978885749c 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:b49af3c29426ac2e39f137bd4ff577250a86ec7b3c34c923d289928b48f68ab8 -size 88867 +oid sha256:58b9692e7ca3fe21d0b7d99d29a57d5561f0132cc8a051e58a15164f7d840f22 +size 91634 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 b6a793d530..52bed8eca4 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:8ef738643ddf9769f360f02a3f6230e6d474317319bb8b9491d7a08118cb6ba1 -size 81604 +oid sha256:1a9978fc6a551e92d64880cebac69380fa57cd793e790529db023111ea1f407c +size 79754 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 1b0a77c24f..1f348e63fb 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:3f3a49ebf327c5695ca978181d72608eedee6dfb9bcbf82c45d74d7eb983bca5 -size 69627 +oid sha256:11460a94377563f3d31bd808e562ced36f87768d2849624fc4f98fe2ab1ae46f +size 70252 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 40e967d51d..f53bb77866 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:5e74dd4977b7f6d5741859db8cb894c82e657cd6864e058ce0ade0330462374e -size 62074 +oid sha256:df52787c1b88af53738293ee058b67ddd08f78c857c2e3af3f006d12626cd250 +size 61807 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_10_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_10_de.png index f1fd7005bf..767dc73fe4 100644 --- a/screenshots/de/features.logout.impl_LogoutView_Day_10_de.png +++ b/screenshots/de/features.logout.impl_LogoutView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30315e868c1e50d290885930eccf724c5d95e5c263dd9566d235a2b1d6afc9f9 -size 31558 +oid sha256:3faff786fbadbc9d1fb919c0c9bb079b035d2719eb9719801cd075509b06bfce +size 31228 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_11_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_11_de.png index 474df112c4..ba34786195 100644 --- a/screenshots/de/features.logout.impl_LogoutView_Day_11_de.png +++ b/screenshots/de/features.logout.impl_LogoutView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f829a007b0b39c35a36d066455b5df1763331a4d57753a1e79ff56aa194f51e9 -size 37223 +oid sha256:b71f67aac5314063bed0e3cff4661a68fef37383df2525f67ccaef1aa28b45e2 +size 36602 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 968a7c0a0c..7a80f64d0b 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:59d17d6f30689416288f99905374e5f9ac2a4faa2adcf07e052883ae350d4e97 -size 47851 +oid sha256:76aee99a5482ab0036faa55dfae992f219e41be53096266677aa65dd9cfce719 +size 47491 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 43a627823c..7deccb7a51 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:fe4a5fceb03be181279b067e578a5d3ffe20d1b2ba746295611ae2cfbe1742e0 -size 33180 +oid sha256:c58d212eeb59ed0a17a8a38e886192a519ed6ea3d868d5894175a3ecb004ca72 +size 32851 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 968a7c0a0c..7a80f64d0b 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:59d17d6f30689416288f99905374e5f9ac2a4faa2adcf07e052883ae350d4e97 -size 47851 +oid sha256:76aee99a5482ab0036faa55dfae992f219e41be53096266677aa65dd9cfce719 +size 47491 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 9f959fb4c6..b3e323af1f 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:a45bd22cef9dc653069d081cdefc89e98f5d4a6abd57ea015095a19360c06006 -size 26014 +oid sha256:a7eeac5aedd7c6de167cb96908a7b62f8dd066faca3ce33b6132df8c48dd1a97 +size 26021 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 a63166bdd8..d97f248616 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:befa9251f01f28dd09063171f7c3dad4a3f1ed58026d5c1dbe718e39edf6982c -size 27434 +oid sha256:dbb085f18507ad5448a6614bf2823879e7e6497a52eb9feb4265a3e717030b8f +size 27420 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 c0d6c96985..b85e5272e8 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:edd961dbf86f407c76386e7e94050984c4f20a49ccda732e44df78755663e357 -size 44041 +oid sha256:c0d4b1c29b4130c1301634cda4ac0c9bcf6ec8f7ca4703b3265bd51a93e58a47 +size 41402 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 cdc5549da2..787d064a81 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:32cbd05a6020ae08b1bc92dfd736734bfbb16a08e281db525f8ddf010ec86c4f -size 42137 +oid sha256:01fd6bc9a856e78ed09690d895527ade133e6efaabe98122be281644904c81ca +size 42096 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 05655e3b21..c2375b4770 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:bd9a0acb232b616300cca31be5c619297a098bc1414a673fb0c00ba29ff6af6b -size 41912 +oid sha256:cf89f910c2d56965e0705b15f502e14073e569a0931e7ea5a2398a663b21ad57 +size 39801 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 b7d0a5b6c3..4632f2d972 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:3bd314ce626efbc0bb31a86012972434a1a7f4e828cfc83c3622303550a0293d -size 55044 +oid sha256:37b929271600ddab0836c758faeabf3a810fab4fc93b6fee1821cfd627aaf145 +size 55370 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 2c63482830..bb7b2152e4 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:507e3abe31d7360c16962484136c69a41e9b8ff36354d442da59ac90865e7a47 -size 55542 +oid sha256:5c0cf0fe1a0dbeb9808fac60cae7a9303254f8004bd8809242a372a468a0a749 +size 55912 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 b440fc9ec1..d53e510d4f 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:1f62483754a8ecf20740d5bc37a00b0aa3b810c005998f22bf8ce5ee7e3cf6cb -size 46877 +oid sha256:0231fe2d6f4d97ff35dea16c2cefbd19bde7e1a9d7814cb1bac5d43cb647ded6 +size 47281 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 ba0b44793e..698df60f9a 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:e63e08e5d9a32dde4f18f4fbcb74744c0e41d012f3d8e68559c9a72555ad661d -size 51201 +oid sha256:1b29b1f2e1f8b339e608af09b9f49a6cb664c9a37130c312c75ca9ab29041665 +size 51544 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 93921db5b2..4d396b0f86 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:d3314f45bc6d2301753aceadd481a35f82399f2a36855e17fe68e33b07132d91 -size 49587 +oid sha256:0f11362ebac1674b839f8adb9d749d9486b1bbd59c71a2ddae6c333cb0cc0a84 +size 49963 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 0d54979924..7759741a6f 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:d2ce1310b330f6191cd3490881b0b3272e296414b492a957a7a2c0fa9f356fdc -size 45284 +oid sha256:18862f4d1303abde8f155f38d9057db7b9d990b288e7587dc58f94636f5927e0 +size 45504 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 8cee3597e3..4b978064ec 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:66cea759a333ac35a55a6ff09ef35f5754ac615ec7430b9a56fe36c9f4e14262 -size 49854 +oid sha256:1882aaff8f678953c313b70ecc9d52d7b0e46ef29162b3cc8a24ced0704a7069 +size 50221 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 26bee2425d..667c230b2a 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:c13ef69c250f617da35ce7e2872afdf427e5b3a5427b3a25d3221950eca33119 -size 46166 +oid sha256:17c547e8a275eefac0de72ca58f6ec3a81e80c8878900a63723c5ccc738270a6 +size 46267 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 03ae9bffac..68a9eeb68f 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:a13eea9ce58656559442de31c8323fabb7e9aee23b247e6a6b9d3b6e5680851e -size 49020 +oid sha256:2299b225ffd05ed2e69e5f5c46c0623a0e54014cd1419ef1ce371b87b803e347 +size 49293 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 c65a66aaaf..6db978f3e3 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:30e913f9226353fee09bd1f6da517494f93cb45ab75247030c3138e1f3ced744 -size 401612 +oid sha256:34c898bbf9bb08717b446664f39a1afdda1aaba0527a92e75555db89e1e7032c +size 401940 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 index ca848e247e..ff47dd3148 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_1_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f092a9b9a3cb8fd1c958015194341e118deafd2589bd34c673747f7383a9ca87 -size 399962 +oid sha256:6fa13a9ae160f9b22eb2a9d5117090759798b93cb8fd5a005c7acc2319a76e2b +size 400282 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 7fdfe64d31..89b6beb4b1 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:f46a69b6a468f8eb0c556858bc2d6cb3176aedb8ce3888d40582e68e2dfd7ba8 -size 61347 +oid sha256:fa105ee550380e5ab6f78cb32211d25009717698c98b8de3dec21bc5beeea6f9 +size 63108 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 c65a66aaaf..6db978f3e3 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:30e913f9226353fee09bd1f6da517494f93cb45ab75247030c3138e1f3ced744 -size 401612 +oid sha256:34c898bbf9bb08717b446664f39a1afdda1aaba0527a92e75555db89e1e7032c +size 401940 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 b83c957c76..4561823c1e 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:184ca0aad1267113df693980073d96e8aba877f2d405c93fc5adff02509bf205 -size 62557 +oid sha256:80b76fc36c84e48de46877503614cd6a6c289ee4defe1d57f3a4381a20560a6a +size 62914 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 8095a482cc..f2979cc741 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:0cf927b792142d25ff50446ca73b0e27b42c686fb2d067289e79cc684e13509b -size 67982 +oid sha256:c06481e9cc1787ac90268137a145790eab9929690e162ef22e7e0b1d4796d368 +size 68291 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_6_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_6_de.png index ee6402016e..6d0ad528db 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_6_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ccbd55eb8a28c638f200aa64b5174b4bee470e1a6fe1ab04bd59857b2bdf896 -size 74025 +oid sha256:5a1c8211e71e899ee932c6016e1d44eceb340a55727b72f5055a7a57ade3e6e5 +size 72017 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 25a96b081c..73fff5485d 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:338a4dd2d2ad1087197e0bc52d108a51c0405b1e6dd2e4b7c5a5a01386d7fc8f -size 407481 +oid sha256:e696c95ee71094650c0008ea618d4688796e8c093c4878ba4735b70487f4e36d +size 409171 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_8_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_8_de.png index 1fc637dbe9..0366e56484 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_8_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd86e863f066b81bbfbbcf7b2d7a0f178461e0d2b741361afdaca1f1515a8064 -size 85197 +oid sha256:c356dd8df9ef8b414c68458828b20a709d55c3ffcd9010482c07e5e0ad501461 +size 89714 diff --git a/screenshots/de/features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_de.png b/screenshots/de/features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_de.png index 4a9addbf76..f614e6eac7 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e04cb4c5d37083a0fb0452dc18f8ed82bddc86136b1fb04aa79d8b7ea2c6cbce -size 57379 +oid sha256:c71d15c6788611ecc0be5f75d64696d7bf68673f9c75c91d692358fedf219ba6 +size 71872 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 bf6614f481..62b61c2581 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:40e5a2393bda1703b749b26d323642cf8aff70337c949a6d543d1b8424af612c -size 65840 +oid sha256:ba1b73e19da856bde24114be1d5429b4cd3cf32367dc168c93105221e550b580 +size 64379 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 975db3ac6c..2f53d77bbf 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:daf09d948b288af931532ac3efac99eeeafd026ff57a11da034ba31950d85345 -size 65930 +oid sha256:e667e7a1db26d4f158930aedff0cf8c3215aeab6627ee6decc9785d5812f32af +size 67868 diff --git a/screenshots/de/features.messages.impl.forward_ForwardMessagesView_Day_3_de.png b/screenshots/de/features.messages.impl.forward_ForwardMessagesView_Day_3_de.png index 415bf08603..9f006b50ca 100644 --- a/screenshots/de/features.messages.impl.forward_ForwardMessagesView_Day_3_de.png +++ b/screenshots/de/features.messages.impl.forward_ForwardMessagesView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8077b421a93fb66d11503d75b558acda35947f5886c0f11d6b81dcf1c5e7653 +oid sha256:384eb7f0964c98a0f9f78085e7523086818f76c1d56e6750424cff515971e357 size 8681 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 index 13d0247d42..659485fa9c 100644 --- a/screenshots/de/features.messages.impl.link_LinkView_Day_1_de.png +++ b/screenshots/de/features.messages.impl.link_LinkView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:205385948204ce81658a86e94c344edb347f495c1a31afe997744cbe0be9c24b -size 33354 +oid sha256:3acf4964ad6d685b5cef831d2397d46beb7f3de7cb1b062b07b513cf4bdf08a2 +size 32269 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 46e364b59f..fb59d26ce5 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:24c8da1cdf528b8ffde0ed71e8931233e6553d0e7331db4dfb318d52e987efc4 -size 26809 +oid sha256:734c8911e1c50f039c90ada55915ca651c1ec1b54ad221f1fefcacf9f5dee9bb +size 26423 diff --git a/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_de.png b/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_de.png index 859a4b74f3..358c0dff18 100644 --- a/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8cb6fbaa752a13e34a7d049b9cfd1048fa499d35cb99dca78ec3ea25f01a4251 -size 25033 +oid sha256:dd115b1d8b6b3d5c302d45110947cee2f9e5b1df9c6c6c7f24f750e1850e04af +size 24976 diff --git a/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_de.png b/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_de.png index 1347f254a1..85c1ed7adc 100644 --- a/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_de.png +++ b/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cd170e61a8802132a308d4e255bc0604c43dd0968c4cca47c850b691b05a704 -size 35327 +oid sha256:a4cceda9f53d1b6a8885bed10ec80b19474e5a587a4e1886553a8ae478ed919b +size 35785 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 908d46d31b..2887180911 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:8794e0e1ea62f3019b4b75a28530fd4d109603ce8b2b1fc8b3e7df7cda24e7f6 -size 48986 +oid sha256:8429bd284e65e21590d48de7aef78a812aa825142e05768513e4111e2a6236f6 +size 48860 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 761354ce2e..24e6f6293c 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:942749be238af7eafb70406a670ef5deca9a9bf3d2a87995a918874b1e66ddd5 -size 49881 +oid sha256:2b28017b6359af7fad86ed48ad680fe6db7f0369b21f4ea580769dd0f9e773fa +size 49736 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 3da8dff40c..c1641803c2 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:403f210ee397ffa1d6ec8c848609d3af1bdf6ea03b0d3916a96b3e380add7a7c -size 49370 +oid sha256:1bc44ba7e3990ed13c1ba3bb546a32dbd32d87bbee9cb8de9e058619de541ba9 +size 49240 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 46692e98ab..28018be9d5 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:d1f2e659b9155998589008bc48e1fe7241112b0d7f85ed2a88338f168a248ba5 -size 49038 +oid sha256:17c1b20d0c2d94225d52bc95fc8ad2ee87f7e614fb69abc3320aa0443e585a33 +size 48925 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 1ca514798f..636abf3e9e 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:efb39a10694cfbb835d171b8cb2687ee20f610d9e614e67ee19de0bfc50b8a2f -size 28929 +oid sha256:4ff29d01ede74dd4bb73eaaf95969867ff27d25fc9123c81aa7746b4c660694c +size 28941 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 3da8dff40c..c1641803c2 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:403f210ee397ffa1d6ec8c848609d3af1bdf6ea03b0d3916a96b3e380add7a7c -size 49370 +oid sha256:1bc44ba7e3990ed13c1ba3bb546a32dbd32d87bbee9cb8de9e058619de541ba9 +size 49240 diff --git a/screenshots/de/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_de.png new file mode 100644 index 0000000000..49936a3ea5 --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ef8f25ec439986622c3ad9bf9a30ece6d3f7e17f264b7260203ee73da4051e7 +size 22243 diff --git a/screenshots/de/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_de.png b/screenshots/de/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_de.png new file mode 100644 index 0000000000..4a867f7cd5 --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5fa8acc69378e1d8b354b14c8c4c4ed2d6e85e931d7f03be81b0e2453654a5c5 +size 6534 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 5cecc78bdf..bbea5c1dd8 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:57acf877ff90191f5cea16ce40af84b5838f342fc401878668fe2742ee22fd64 -size 11747 +oid sha256:a7f2af2e1a184964c69e6c01199820f0fbf7d36734b0a76cb6e842c2027fd07a +size 11469 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 f97f8f7e1d..0236701aee 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:225a74373cbaeba3b2c7e801d37c5575be3c18987e3167bd75e43d35148c0fd0 -size 20107 +oid sha256:cbea44bca597ecdb6598dc082faba460bf339f0070024187e0f376b791421af8 +size 17045 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 a90a1363e7..c8f4f21234 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:d0f8d342ef5e3507a0cf0c7ef8fd78a035c529c66085d8949c8f4cdbd5dde984 -size 25349 +oid sha256:0fc6105449a2b5bca7085ba8ebd4766b0aeca4b812439a3bb6031876ec528a7d +size 26267 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 2e8febbab1..5c6b0c1317 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:ed229f3db2609923e4f38435dcbf6f3615f6451216f9ac599fd42034414a0107 -size 17933 +oid sha256:19823a48ab2deb3d575b076f684d99a4aa22cafc514d8618d728be3066ea4c2a +size 14939 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 0ec9e19801..dfc7df4bdb 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:99a208301ad2252b61448a3fbfce6e3252b592c3b04e36617b2207532d982fcc -size 56626 +oid sha256:36c0a8af9d4b4fb983148c6c4ed081eb5e6f9a312822feb827deb39c7a970905 +size 56490 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 9e8ad7c3fb..4eccdf182f 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:27ab4da95563af4f7063105b172abcca812dc20702eb76817fc8656466ed4989 -size 58576 +oid sha256:a93acf48045d16060cb04a02cd86c1fb474dc55c61d6f56e3fc35abc56d0736e +size 57920 diff --git a/screenshots/de/features.messages.impl.timeline.components_ThreadSummaryView_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_ThreadSummaryView_Day_0_de.png new file mode 100644 index 0000000000..9f240daeae --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_ThreadSummaryView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18a7f4dbf3a56c49e5b1edbd0f419620dbfcd4485bfe25db5347968b8844973b +size 9933 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 5b1ac87129..35a6ba8efe 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:dfe87b6e8a57b8f0a72c28acd409df6be0be296f265fd9dffc767c3643a22fbf -size 38400 +oid sha256:3b75ff102cdeaf138ae9b5574391a348c0542c486a1e36f2bb83ef9e8c47ed30 +size 38097 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_de.png new file mode 100644 index 0000000000..0de6e5f19e --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8b14f1fba1b0faa17b331d442e780456f7d0968e50d46d4c4352ff56d2a7bda +size 68148 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 63e4d2887d..01dae86c2e 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:f8014023ac0a265d62a0cc00ec70c2c2445d932980a1a046c0bcd8be4c640090 -size 8354 +oid sha256:57d4ef7e08ddf1121f50c4840c074c7d0c6482697e0ad3a8528dc902e5716f54 +size 8788 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 fa4bdef179..18c7589459 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:514b9acbabfe44a7c884de2ccc2ff381bf20a9730a5a003e1ac8d3281bc6d642 -size 17803 +oid sha256:4928b731a26c8119c34cd1d8423b1c91eacd6ae1eb3b6d50f928f3fd42ba31b3 +size 18222 diff --git a/screenshots/de/features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_de.png b/screenshots/de/features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_de.png index 1cb1173b12..c53dd3597d 100644 --- a/screenshots/de/features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_de.png +++ b/screenshots/de/features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a83be835af4ba5220bc17d56e430b53b363df5a53e7f4a05fb34cb0e7e9487ff -size 11513 +oid sha256:a0a43b9a921e9e6e3524f3830a4a6d8a0ac0a8185f60427146e0a683c1f68e5e +size 11516 diff --git a/screenshots/de/features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_de.png b/screenshots/de/features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_de.png index 1cb1173b12..c53dd3597d 100644 --- a/screenshots/de/features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_de.png +++ b/screenshots/de/features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a83be835af4ba5220bc17d56e430b53b363df5a53e7f4a05fb34cb0e7e9487ff -size 11513 +oid sha256:a0a43b9a921e9e6e3524f3830a4a6d8a0ac0a8185f60427146e0a683c1f68e5e +size 11516 diff --git a/screenshots/de/features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_de.png b/screenshots/de/features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_de.png index b23fe05d44..c856f0e358 100644 --- a/screenshots/de/features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_de.png +++ b/screenshots/de/features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:628cc87a2c18dd8416ddfca2a7bde83f9254e79031bbfdbbb41663f3ebc6d0b4 -size 14401 +oid sha256:506f037d7cce42ac09efc89bff5d74475a723321af4713ed54a7caf85bfcb297 +size 14382 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 148934c81c..83172bfcb7 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:160988f65f378e1fbaa91b135310cd9ff2f88ccd9975da711e408000c0a6e2b9 -size 38064 +oid sha256:dbd684f8e72f659abedd2a4a6fee985814145089ed4e167cac950119ff1d2aab +size 38009 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 85d6aa3afe..d1e96eb107 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:653272113d51da0d44aed06b3c3a276ab802b5760f29958d4a61067be7a98433 -size 51466 +oid sha256:4d098e36c94526f6b7d211cd7f7d094912f1148b4ff2f40e82c0d48b0633ac70 +size 51750 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 d6b0448647..154e729716 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:4e8d184f12cd88cfdea5513735b17280160a4f91dc9151734f62646e26dae0d7 -size 52913 +oid sha256:9e9be49fb8ecbab397aa9ca1d608d576d2549924bc5be05c2a47d3a76c2e0ba5 +size 53186 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 222897cf1d..f8ebb81541 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:6974e77921a6529c5318a96139b29e7e1c4424854b7c96720709157608464427 -size 64786 +oid sha256:ce663c192fdc65b91f61194904da3553eb605630c6f7bf47f9853d3945584a65 +size 65076 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 bbab1f75cc..61c758aaa3 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:085820af2be18893fdc550e8bee1659adb47c98cac6db4811384ad616d6f4dd7 -size 49406 +oid sha256:14facefce7d332e13fbc67109b3b4fff22881bb64c4a419094f5fd2371b2d9fb +size 49691 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 da8481a4e2..03d2f3f77e 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:5314a0a7f5fd861cc5506667f55a77015a1d4e05daa8de62e8cbc2d45a3a84eb -size 73361 +oid sha256:a004d13252462f52a0f324cca838dc48d6d773e61c235fde33ac68ff5bac9e52 +size 73672 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 68d8d7bc29..9f08b2943c 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:9d24241330856354903a289dc2d592c022e5255e49559a01ab9dfd8bb346302d -size 59055 +oid sha256:5b705e80b55329df2d6786c8c619100880ba7de6a36602911857acf55fc2691b +size 59323 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 af245b2b63..a8017a2f55 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:5a8a0d272b21e744e8daccc75ac3a992b2f741700b33207dac74ed2636e96af0 -size 65803 +oid sha256:1548fa74ef4a3cd52599a467fe91d69e921559b24536ec4e258579ab853cb83d +size 65828 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 9eae6a8719..04239eb227 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:e1db818b5351ec948425304320c2e7777b9df0c2b562fb44ad3ca153b5df6701 -size 73321 +oid sha256:21c6b7761c4453d3499d9841df292ba7e6f0703f41677351fe47bf9f3f038c33 +size 73626 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 b980c00c49..4511b92aa6 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:75ef083e6587fe9b34cb432b60fecf7f00f67bd8785b1de65a35d8cc823e1595 -size 71726 +oid sha256:203290c665873113dd97089cc1b4a2eb169fe5f57ec55f968ff5f8f90b10e091 +size 72026 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 f33553fe93..ba4030eb50 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:e766fdeec8ebfe8896a5860abb793cf51f6f8233eb251bd353f25f073cc18d18 -size 74824 +oid sha256:a977db6aa54185e160ad87850205d317191d51aa8dc24839dda3b4f96a2f4c96 +size 75119 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 986d5b788d..2139198bc9 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:77a0f8c082360c16e18f7fb0ccd88b7ed5f9eda0c71fd7428376ac6964f39f60 -size 55067 +oid sha256:5cff768089c036e015d4e162f8e7a55b026a0b26e84d08fed85cb336adb8bd9f +size 55396 diff --git a/screenshots/de/features.messages.impl.topbars_MessagesViewTopBar_Day_0_de.png b/screenshots/de/features.messages.impl.topbars_MessagesViewTopBar_Day_0_de.png new file mode 100644 index 0000000000..d6e752b972 --- /dev/null +++ b/screenshots/de/features.messages.impl.topbars_MessagesViewTopBar_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8348ac5ab0a32d80f7566bd734ace5ed549d55e1d003571e0e7fe3a68986f7dd +size 42668 diff --git a/screenshots/de/features.messages.impl.topbars_ThreadTopBar_Day_0_de.png b/screenshots/de/features.messages.impl.topbars_ThreadTopBar_Day_0_de.png new file mode 100644 index 0000000000..77c579fc16 --- /dev/null +++ b/screenshots/de/features.messages.impl.topbars_ThreadTopBar_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f05ab027f2840e839337a59656cbf3b0f526e45ef8abcfb4677585a559e14fd7 +size 33920 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 e4e0f50f76..92fb37b785 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:2e1c039c775e3b3ace5c4f6b21b91ea027383f007a9c8c4743c3485c05aca2c8 -size 58976 +oid sha256:5969d4a287e7a6f589639569da53d97c8adf7c190d05776f5342084d4344c886 +size 53839 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png deleted file mode 100644 index d0b40bab37..0000000000 --- a/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:12e6c4fa5f0fffd03a9ef247a41d64e3ac579b6a5b803fcf0fb89fcc71bee9c7 -size 62309 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_12_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_12_de.png deleted file mode 100644 index f7cba2d2ec..0000000000 --- a/screenshots/de/features.messages.impl_MessagesView_Day_12_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d8d3649c61ee7d08f83426fc766819663302051149ada8e4cbf612be9557991c -size 61237 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_13_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_13_de.png deleted file mode 100644 index f64dcd2e47..0000000000 --- a/screenshots/de/features.messages.impl_MessagesView_Day_13_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3d699f41550104cd5c8cb5744fffd2f966b60f5cf8a8472e17efc15c2df25346 -size 61165 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_14_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_14_de.png deleted file mode 100644 index af9bdd9412..0000000000 --- a/screenshots/de/features.messages.impl_MessagesView_Day_14_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e90b5cda886ab4fad45b65abfdcc909c9aac0304ade76ce95e114a381a9fdef4 -size 65750 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 d2f2902674..1641b39414 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:303369a96f59235a88cad88bd022cbaab8063f865d78e3f96ab5e91699e76156 -size 42112 +oid sha256:5605bbc41c6270eb6cd44c3bf993a6b5e9907b5dccb9bbd3834e4d7eded97198 +size 41782 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 1c266e7548..2282c34d85 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:0a94ddec52651815c17d8855369d87d7fe1fcd0f4c5de53d52dc7f083a6fea3a -size 61238 +oid sha256:73173fa138e715f2cb0f9f7950208b6d3ab5d1a56f054d514ad24ddf4780a77c +size 61245 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 4e2639e982..af8d121795 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:ef763e848edb197e68535250e4bd14a073446d681156107f7ce5998d93b16cfb -size 57312 +oid sha256:3395537215d458bc647028d7b338e91fdf94cb242033afe063e165d9459c05af +size 57444 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 cdd1236c77..7c1807d837 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:8e117ec0cca4db612187634af65fa06a66f2c42b4ae4f85129c6b1e1c8d26bc8 -size 60432 +oid sha256:9803a08e9c42e821941c85f596e909755723680f75534104f297a6f2bcb37a7d +size 55309 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 7c1807d837..9a48de10cb 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:9803a08e9c42e821941c85f596e909755723680f75534104f297a6f2bcb37a7d -size 55309 +oid sha256:9ef54103492179e8003b304ec1554873ac031d18b1bb00aeb65767b403b58915 +size 61819 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 74ea65abd3..93a2e19fca 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:c3610695c30f6dff010cac696623af946afc1e847a54af48dd71cbeab83be033 -size 61433 +oid sha256:ceff1004db2bbe6cbdf2c6d62e4521b89a9c63823b6cd1c709037e3bae306302 +size 49568 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 0bd6113fae..d0b40bab37 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:b206dd36fff869ebbff8a63c783cab6f24998458cd6bfe8af397a28209cc7894 -size 61425 +oid sha256:12e6c4fa5f0fffd03a9ef247a41d64e3ac579b6a5b803fcf0fb89fcc71bee9c7 +size 62309 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 66f1d91843..ea6e3ef99b 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:d22db7b845a7fe310bcc6d8c57e95f32b1edfb9841f9785fd9f3dc261ad7f48c -size 49579 +oid sha256:2439aecde9e9ad584cc7e06c6483d66af120d5abbb4915381be6b091f51168ca +size 66206 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 490bba6331..2500a47e29 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:83902b85de7faaf3f1e98ea1969d69c39543b37b59afb1bf394acb02992a2b9a -size 42892 +oid sha256:2f7893ea8ec10b5a7fd2d335f498a637602e0a0e30fbf5a9368a5cad150bbede +size 42554 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 6819864e77..48641b50c0 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:b8ef9c9d0ac7c653b0ea36fd56148c00bcf9780ba848e9cd71a54608610d9ca9 -size 40881 +oid sha256:1fcf6e7dc35e868539efe503ed937d978ed75c5f3f46e4f19ab52dc3bec71303 +size 40813 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 676bebbc66..b0887fba96 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:a0c7748f8e99a26d100b1d0c93ce08bcee64e588aec20e797003c17eb7133b70 -size 60980 +oid sha256:07769008e1f31eba70a3800f040d43066f14a4e004afcff7093304a24929504d +size 60145 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 7385e44a27..18922dc7ee 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:2ec053c179d6a7399f9392bf5a0c45bc90d6e99bd36c857f7a47a87f14ee3eb8 -size 65160 +oid sha256:7e7dafa33354b3b8eb6cc287ef56d4cd5374d3f1060f0c95c32a0ba4c23c91b1 +size 64360 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 26d501491f..6ac61e0431 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:7807ed8de2cd2d555dfc53caa6bc8424cb66c666f862741a877aa542e66c4e62 -size 18143 +oid sha256:a8f085fd176b3136ed1f75c29e03ee7ccffa0f1969b19f1951edf60da425e56c +size 17233 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 e91b767ef1..fe440a1f2c 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:c1f233f820bf61eb6afc0e5945ebeaac78bd5710195d1b85e2ca60de49a86cd4 -size 18304 +oid sha256:2bf68384e86515099a8d4b1430a6f03ff54829317bdd9ac949c77270e57d953a +size 17451 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 43463016d1..dbe6078da1 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:f31983cbdbdba5806948867d6a81290ee254c0d45e9b146bdf109b49c8b850fe -size 22017 +oid sha256:b5b13f60e9b9363d183f92d278a39ce49e59d31248de9e027fab6dd3287ce982 +size 21162 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_de.png index 1075303134..18075624e5 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7d95381466fcdbf56b1d8d71d6723b85ef38588162073ff1864be2b42b54e41 -size 53199 +oid sha256:640fc6cb730521e831bc408c13fddcb16d7fc5767be0599bd14703e135cba673 +size 51711 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_de.png index 88211b5725..7e8d30e121 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3e42100b18224448f38dbd0b38af7836f452af76f21dcf70b9c7453aacc1f1a -size 53061 +oid sha256:5e1147eb6941bac62236bf6b10cdb8f08f6338a89567916517112aa4b0f48939 +size 51577 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_de.png index ea17a271f7..029582be59 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a8cab799f06a939e3dffed6aa8219f8404f855b4ca4346ebe31341df928e68b -size 53042 +oid sha256:1d7d650a209b796acfa2dd8f5582de6792609136d9048a7ce6e87ec69a37e199 +size 51553 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_de.png index e5fbb99e20..0ead90491a 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:232f67796b725b1e3c304ae990537c44170ea8c652fc16a23d08ea7fa4fd90ac -size 53044 +oid sha256:57e30dfd18e347b047328c73d011b70f65285fda4185a9ba8b1f4f0b5f33b81c +size 51582 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_de.png index 0ccf3d412e..b1503ea24b 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ea723bb7ad080e674267fd126beeaa60eb9b6701a0b994b2e5f37015dbd5f38 -size 53000 +oid sha256:37f4cec6c62f23337bf8071bcd784a090cc3da0ae6d965bc303b489d5c3c857f +size 51430 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_de.png index 91a8a4c501..bbeb292cf2 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6be0734e9ffc6742dd9f7419a1fb431ec86bf88d8aa1688140cfc18ce5cb26d -size 53193 +oid sha256:9210fdc033816afc90cb87ed3805f7ef87f255b83059a330816efab67b421d20 +size 51705 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_de.png index b3c5ace2d3..bffa4ec55c 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6f0a98e9447e7df39009b636f6a30c7b4c9159e9266da1e6e429d3eed469729 -size 53013 +oid sha256:994c0459cf57b9cd03f666389c38e745e1885909d89ec26484b1482d465bfe00 +size 51387 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_de.png index 87042b6690..5fdde723c2 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a056f00d26ccce970f5f07792e098131d14c280d475f075d2430a13db234002 -size 52626 +oid sha256:ba0513caef897e5f759f6b57d52204adaa4754741ba8677c6201cb45290cf258 +size 50932 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_8_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_8_de.png index 742fb71866..6cfb8fa904 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_8_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b738736bba6f447f53b98801bde3dbc0eb1636299ed5c2d840e236ce0ccfed3 -size 58457 +oid sha256:fa4223f158398f7e8318d2dccde9ad5f19c6ddbfbecd56195cb33ca31b611a65 +size 58877 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_de.png index 2f2b6bbe81..7c7382892c 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:704edee77c98a75ed521f632762f8527472c9ce3948bd30cf11b507ae97de95a -size 55569 +oid sha256:9aae1dc71cadb6e66f6e8e0442ba2930d763db9dc0b37ebc08bc1bb55ccc2769 +size 53769 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_de.png index a2099c78c8..76b609e927 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de47269ff2b05c16d8dd52300093836450a530685e316790db2ed3e78a9b91fe -size 55445 +oid sha256:d80855517f8b67231fe586bdf3b5f093fafd31473e1df26133691ceaccc7cc88 +size 53647 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_de.png index 0469dbb456..1bd59b2eb5 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e728c95140407d0514ef8326b6acb6cf5c08d9c30eac7eee5a0a0a505cf5330 -size 55450 +oid sha256:37cee079ad400733cd5e79bf7cc2ca8e57bd3c943718e8a0141f40c05876d81f +size 53651 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_de.png index ea4287cb52..96d13a3559 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b115346137a49980aa7dc348ff7bef124039c23aa1f696c3bfe819fa5b55674 -size 55451 +oid sha256:93cbe7c58a35811ea6ede3b159eb423b450ffa0394ac7d55c334c3222a65c077 +size 53648 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_de.png index 80f3b0d3e8..0893c5aeab 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53eb86b0d2255e7133d85dcf3a00ea257a3b3d3d383928172dd6f9f84ae3cfe9 -size 55334 +oid sha256:9c34f1a555c5c4eec4aa836cdc2e51ae3e6af073b596b6759040e329a8d6a65b +size 53551 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_de.png index 180e0494b0..caca68cc7e 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ab683461ca76303745a51f7f8028e8daf44408f9140fd5ff28917022c55f69c -size 55595 +oid sha256:61e20fdeccf23e529c4c88d008a259bb287bdc9d45d3fc79b14ec43e9f33b96d +size 53791 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_de.png index c6ccd21905..bc9745c65c 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46f0e21703c90c36ec78f4b3f5291f52eb5202f3c8715b99e48375f1cf38fe4e -size 55196 +oid sha256:9cbb9fb883cc6763109ab6b8fa8a47f3d476301a98ae4d323124486612fdc50d +size 53481 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_de.png index 49c234382c..b8f8f80b22 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cfb2fcf2f2a923d7cfaea769d81bc3f6ff43f2d34c57b15a44f3f8f431801cc -size 54663 +oid sha256:8caeef8ba4c9429da99219cd4384e2f73e4616599c7347de7f5d7a4988e9df6f +size 52959 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_8_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_8_de.png index acaba03f33..faddbcf144 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_8_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21a1dd8fee9fe830054738eb7d02ab047f11e6223510bf11eb37d552d746109a -size 60846 +oid sha256:b33cff6bed2dd649aea96274dd8f837488d966ff5e06ad5484521a096a269bac +size 61633 diff --git a/screenshots/de/features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_de.png b/screenshots/de/features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_de.png index d092ec4d04..b8ced31a50 100644 --- a/screenshots/de/features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_de.png +++ b/screenshots/de/features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:793ed7064eb52ddec72ba21e506882f55493390a11548df5203c0ea328a32d3e -size 50111 +oid sha256:8e9642babd75110581efedcf740f42d800dd8b93ff63707cf54ec273a23e6ce5 +size 62459 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 72ea2b6774..0190896940 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:6eed0e936eb7b81494473d9cdf5d737b4bbd4d92ab067b2b8881b8d3dfff4685 -size 28910 +oid sha256:15a7e16b4731665934b7b7243fe08b226a6a8b1ab9210f6376f324c961cacca6 +size 28481 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 b70b75eaf1..dd66adc2a0 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:cbd5c2113a241469a7108d3370838b96e6644f9edc60dad53c9cc9eefe231f24 -size 42820 +oid sha256:024f708039578f784cc1d52bb7c7c6a8386a2b0b589f719870d620afa5c6a4ac +size 44188 diff --git a/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_de.png b/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_de.png index c120e1bbf5..c2e54513f4 100644 --- a/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_de.png +++ b/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3d871fa1ee1eebb9f2edac3a75f4a0b06dbc6ec04569e07697d49d603a813a8 -size 50540 +oid sha256:50738f74443668d50539e34bfd9581330b393ee1855462b392ae741f7dae9dd1 +size 50563 diff --git a/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_de.png b/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_de.png index b4cf3e1ad4..881b77d38a 100644 --- a/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_de.png +++ b/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fe36c7707f907e7d3e8cf7507e536c745fb43725f6d250f3818034f1bfc0411 -size 51263 +oid sha256:5da51224d52c42aff7bea1f0573194170933da0141021ca506c338ad6f68ddee +size 51277 diff --git a/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_de.png b/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_de.png index 186458f2f3..c7b4a62bae 100644 --- a/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_de.png +++ b/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2e69cc6fa03bee4c3075257662dd6eb2ee2e6ae2b1e4f1b8fecb66b2654814e -size 42247 +oid sha256:229aaf6b2a92bcd43c97cfa9e77c4c75b294a587306cea1d92c5b65208b5a97b +size 42261 diff --git a/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_de.png b/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_de.png index c91f3101bc..b447e6eb76 100644 --- a/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_de.png +++ b/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a856144e5645d4624b888be7b7a0b2d7be3f40b86e01893347f700dc0e1f0acb -size 41411 +oid sha256:d5130de6e57ac0b4ada7b7a7de549cb08c1a1b48ac64cc05ab39f8d2efb41680 +size 41413 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 dde4d0b9df..3a9acf603a 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:1a9373efa48e8989e89972a7b4ce78d63ba4fcab3e6d1f89e999b383dd148ce7 -size 68481 +oid sha256:17d720145734aed7fea487a031712556382617ef60f97405569979f153e39af1 +size 69922 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 ae4eae6136..5837583cf1 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:33e046c38a374200a1cd8143d2d8ecb9b28fab08d58a9207df5f48d26bc6a61c -size 42847 +oid sha256:bc683b3b8b0b3ffa9da10e505559f98b2faef0dd85523d2bf21faa32956d9483 +size 42832 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 e0c8aebf51..217e001190 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:4143e4ab5b6ed230ce2c7eb87947a9d45415f5bb412d22994921a0e3a0ad45b3 -size 74280 +oid sha256:34fd1153d2a1113ab211cdaaa4ab7a40a7683d2dc07cc63a5ee833c80e9fed7f +size 72473 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_3_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_3_de.png index caeba08e69..589bdcabc5 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_3_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bab5d7148aa93d93279ba04125814702664ffc272d29e5c7288661a2fe45d4b7 -size 50021 +oid sha256:3b8df24928cda90d3e29186034cf088ebbe37a75b99e7ac8132f515e339a6bc2 +size 50019 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_4_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_4_de.png index caeba08e69..589bdcabc5 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_4_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bab5d7148aa93d93279ba04125814702664ffc272d29e5c7288661a2fe45d4b7 -size 50021 +oid sha256:3b8df24928cda90d3e29186034cf088ebbe37a75b99e7ac8132f515e339a6bc2 +size 50019 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 9bc0942c37..0fc4259316 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:937d957f5ae3a1eac0255c6b49d66fcef7faf6abcd2454959a6b5d94c526d154 -size 47305 +oid sha256:b83ce1b120f5eca77007f7f7d957bdd3c45135f5f3e454ec68c50564924373f3 +size 47163 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 93dde7fdb7..663000bae6 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:69ab4f821f3fce81137c6ef4ef73f28402003bd5ef5d89598f91e0c24d47ccb2 -size 52423 +oid sha256:633b2df37189860357e7a98285f57c098e580d61cb82fdf274055e7559865a9b +size 52264 diff --git a/screenshots/de/features.preferences.impl.root_MultiAccountSection_Day_0_de.png b/screenshots/de/features.preferences.impl.root_MultiAccountSection_Day_0_de.png new file mode 100644 index 0000000000..2ab16568a9 --- /dev/null +++ b/screenshots/de/features.preferences.impl.root_MultiAccountSection_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2505c99e7ce75631696f09774c312363d414856bd2cdeece40a5bbfb04f38184 +size 59248 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 008fe3e36b..7299318462 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:6d88d7dbbe66c64573df0e237e613be676533628bea149fe69d5631a098f4d18 -size 40286 +oid sha256:b93b156050fc9a375fc33cb2f90dc26d7eb8ddd89631c56e2c7e6bb3bc3bf643 +size 42013 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 096c9d3ada..168d24f6b7 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:7010114695842036c447c44a91d194fcdb6423d8e6f50a0fe82237e8c896b877 -size 40136 +oid sha256:55266e1bc99b2d5f05c722f6602670a349ab4d88e81f4edea2b7802de55ca1bd +size 41836 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 94d0c20377..5a4350130e 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:3236e2feff08195a5c02daca103aa6619132ddaa9f0b7ae0daa7fd048319f91c -size 41311 +oid sha256:959ac8983914b300abcc2e0d3dd8e167d8f2ebb400ad4147c6dff0d3f6be1435 +size 43207 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 925834be9c..71d0217e2d 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:58eabf09ccf68f7d2157a4ce629a2ec5cd3f07ee596712adf5d5eaba0b7d028e -size 41361 +oid sha256:bd504bc86cb0d2e36a918911726bb9ad52aec63627fbb8af4c3a9fa27fe8f713 +size 43248 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 68425c4a68..27c5b3ba38 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:8eaaeddb4da0676da125e39d15ee60c37ba37e46add4b2c4fd16015a74450ec0 -size 24397 +oid sha256:2ad73144a629216c8878a437b658e153d4694cbfeeb54e156f0a4b064da2642e +size 24330 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 d0f89b8d0d..33b1169ca0 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:99659666c78a2c1e29d73cef4996f3bf6fee971673d8e495b8ad9b0033df153c -size 28573 +oid sha256:1a699610de5b54fe37f57a7f4d7db5973358bc683bf85e8ef632bbeeeb8aaa17 +size 27802 diff --git a/screenshots/de/features.rageshake.api.preferences_RageshakePreferencesView_Day_0_de.png b/screenshots/de/features.rageshake.api.preferences_RageshakePreferencesView_Day_0_de.png index 6161fc6377..855457bd1b 100644 --- a/screenshots/de/features.rageshake.api.preferences_RageshakePreferencesView_Day_0_de.png +++ b/screenshots/de/features.rageshake.api.preferences_RageshakePreferencesView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abdf9963b18ab4012ef0ac5cca08f63c9c786ba5ec1076a970af5e350f50bdb7 -size 20382 +oid sha256:28443443e0be59a6862823b4de56adb60c6d8cd62b6abe27729bbfa9163ab52f +size 20306 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 d557be2089..2820191c87 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:31b5fcb9819032953044a7d89a0152e9b3aaefcfd228d56c89c56aa87f6bcdfe -size 82237 +oid sha256:818aa8617bd3866d4643ded290119eb1324cfe011c5bea327828b02f09756d94 +size 79283 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 32e0d7c717..e300e1fe4d 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:4d07389f215f10d17ed91a28dff47fdf463b74229ab6b923f634161c0366d973 -size 88478 +oid sha256:8a8fc1f3081a7565abf7bb9c8d0d012ecc5981b86f3d60e77febeadb5dd2c46b +size 98560 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 b52a5c9119..11b865af46 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:b02411da769b90c38aea5f270971a98bdbddf3656da171cda7d1f82bd210c33a -size 78011 +oid sha256:1939faffe9d5e8591462d4c5302c26d969be2560ec4e954913a59d9ec1f9601c +size 74937 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 d557be2089..2820191c87 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:31b5fcb9819032953044a7d89a0152e9b3aaefcfd228d56c89c56aa87f6bcdfe -size 82237 +oid sha256:818aa8617bd3866d4643ded290119eb1324cfe011c5bea327828b02f09756d94 +size 79283 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 ae6dd1c6c2..66088d0cd9 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:ef30e0a39c96edf396e21260fbe9f733dd40c9bc81ceb1d4c9cfcaed5ff03b61 -size 59262 +oid sha256:bce0a946397aa0223be195fc0090b6bf009ed02a914879840bc445b271a37a32 +size 57646 diff --git a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_0_de.png b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_0_de.png index d58ffa1db9..2b9365224e 100644 --- a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_0_de.png +++ b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04ad845cd8bacebd68b245d597d0f25da92998b938b1fd4d7cf5e13e8d5e3dc3 -size 34184 +oid sha256:17152083f68f5db4b3badd5bc599456ff0f56e2e85baa3b26124b302ce4b52ce +size 33635 diff --git a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_1_de.png b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_1_de.png index 488c17c778..822822882f 100644 --- a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_1_de.png +++ b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30a8a1ceda098da2ae406dce8cb0df6158f9311fd6a0b88967b992cee5e02b14 -size 33931 +oid sha256:aa4ea00e96212dae3bd8e87e96c0267624a3c63574dd6c279118916a38391860 +size 32194 diff --git a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_2_de.png b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_2_de.png index f258693f01..3ceca63e8d 100644 --- a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_2_de.png +++ b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e63383829659057c7e7d776b2cecdef1d401b89bd733597ccf287eb019407a4 -size 33800 +oid sha256:290d26a59077e059d04914ef3dfc8d5b19685c5fa447b54d2066fd8a901f5853 +size 33255 diff --git a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_3_de.png b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_3_de.png index ea59eff8c5..b6378c2b9c 100644 --- a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_3_de.png +++ b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:005913de31e6b631050bc62349ec032ec2483e8e60e318356b9a733e3360e132 -size 30514 +oid sha256:d7341fbf53fbf1066bcf75691652782cb549b9ffb715c0b80d8c5bf7dedf33d1 +size 29213 diff --git a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_4_de.png b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_4_de.png index d47f6cf4e1..dc2fd86233 100644 --- a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_4_de.png +++ b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d75898aa8d869e3fe094347fa113b9a4c6d048b27a7a438d1a53dc77b6c528a2 -size 33382 +oid sha256:43c3083501e9fa128a2eab0896829f1a37ac673111d2ac7cbc3886ee1a02f884 +size 32001 diff --git a/screenshots/de/features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_de.png b/screenshots/de/features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_de.png index 4e83319d93..16e8224c33 100644 --- a/screenshots/de/features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_de.png +++ b/screenshots/de/features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27398dbb52e4966b6e83838bb6316d62244419988e7b322dc36b3b1a1d821b96 -size 35003 +oid sha256:544cbdde4d635c702e64c4279923527a10a5fc5bb199cc074e44d36208dcf1fb +size 33946 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 10383102b4..05d32f6e21 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:ed6d3b982c3a7c7b50127e5abce50fdbfcf5e83b65384a78a37589ad02347b59 -size 27025 +oid sha256:46650e2769751b04dae2cc0edf05d628678dbfa73439d207f3cca75379dab8bd +size 27744 diff --git a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_0_de.png b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_0_de.png index 8d8cb08feb..6bf7743e5b 100644 --- a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_0_de.png +++ b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:440c8ac518d9f29d4090c7a8a3760c7e9f5b2701b7fd8e7644a8bd5b605b6cdf -size 29874 +oid sha256:db034b5bc00f6e71c37ed2f180d3030e7b0f98f5247eb3f538566b82a43c8687 +size 30026 diff --git a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_1_de.png b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_1_de.png index ecafe48b38..df7331e342 100644 --- a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_1_de.png +++ b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a97b8867c2fcf6857bf1dc1eb6666a6ea3f44b0b25bed421c68c4b5bd251424 -size 24340 +oid sha256:82bcb78fd3c9b508862c2494c94476e74ec666eb6ae1c096129e6375bbbbe65d +size 24597 diff --git a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_2_de.png b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_2_de.png index b39299ae36..65ab5399fc 100644 --- a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_2_de.png +++ b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa77df2826f0ca4652bb8350015d7b0a4415987dd8c04c3eee9b990ff8d10787 -size 31005 +oid sha256:218769a104db9edf946f52ab66860e03fb032f5b3dddc1c95d1c0974466d64fa +size 31158 diff --git a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_3_de.png b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_3_de.png index 92428ea447..3d7f71518e 100644 --- a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_3_de.png +++ b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:baca20b567aba156274713fcb2cb2c8d1b8c8dbe2a601307cb50406a8f283432 -size 55453 +oid sha256:e2f19bcc39caeaa1f88862149f53b375d78c7e56e1ce908a4d2188a55fed76a4 +size 55617 diff --git a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_4_de.png b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_4_de.png index 1d1883cfef..495cab10cf 100644 --- a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_4_de.png +++ b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cf6507a7e1faf2810d78975cd1a57c60f26947756c15092f710f97d46ac900b -size 29903 +oid sha256:062103c4a5ad33a859854da6c5634312abed1a9dc0b756cb23653c47606ba8e8 +size 30058 diff --git a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_5_de.png b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_5_de.png index 958f7fa7b5..8fb76b2c20 100644 --- a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_5_de.png +++ b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7bac438b9ad8ecdd3e7564b789b60be0ed10f2457bbdd4cf6dbe08a3694e5dca -size 29996 +oid sha256:192516d8448d0a5b65c8208a0eefcaae922913fe8d2e859cb7e0dbababe4079e +size 30144 diff --git a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_6_de.png b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_6_de.png index 96d844b747..75be860ea6 100644 --- a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_6_de.png +++ b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9058425823abd5abadaafc1a56029bb875c908d2934595fd989e8ce33c1692c9 -size 27067 +oid sha256:52effd0ee06fd2a7764974dada7fbbc93453eab2e17f58af4b004d7182598514 +size 27638 diff --git a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_7_de.png b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_7_de.png index 5894ef3456..87677b8e2d 100644 --- a/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_7_de.png +++ b/screenshots/de/features.roomdetails.impl.edit_RoomDetailsEditView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c5d85fbb65cdfb9737b610b42f3577c0897a7e30b4a13c3251c7e54545f7252 -size 29662 +oid sha256:445db5ce8ac1d90129d2e53be3074f0c8d4518374355833c0ce3579fa9ff3394 +size 29761 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 new file mode 100644 index 0000000000..949ec34d43 --- /dev/null +++ b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b580adefe5d563c71caaaf17639b84b7e6c8f5eda5c3d154cce9a5176ed7825e +size 24822 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 694525911a..2922988155 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:9f46825e26cc45cbad2bf8ac0e7fd6bd9b2673d3f4501fc207ec7615177d38f6 -size 46967 +oid sha256:63a9eab94985fec27378400856858dd2a6899e831919d57f3b0804254e521056 +size 46007 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 5e8894adeb..d9707b6ac9 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:b33c58b81a6d8f0211676b39174f27b8c01e174e4e542ce6399b56a0badfafa5 -size 48117 +oid sha256:3ad3c2694bb93a5bcf3e1e2a48bb96e3cc4a60a41e1fcab0adebde63b9595006 +size 47203 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 898cc21361..1e4316618c 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:22cc7a6b738474421771493f9dc9384b0e9724218abcfb6a6a3456cd69def558 -size 25533 +oid sha256:0a686e2024b1de0408f0d5920ac576b2822677dea98ab85cec11652edf3eb02b +size 24681 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 ee700f5a30..f989a4b7c8 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:298d97f3ad8fc474954ddbd9bc729ef1c0c3d5ecd57eba99be07d12ffa509d9b -size 36770 +oid sha256:afc06fbe7332506b24afdf8283fbc1bf4452dce15bd4261647cc5857ff8939a0 +size 37974 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 9d04c2dadc..62440e7612 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:0e5a70c38ed47672d570d9afdd3b8e4b188a9ab90cd2983299c10c7f765a3582 -size 44232 +oid sha256:e0ae03eeb528590d37def020f34e8ab138e16be7118c8742ffb5e5b0401ca91c +size 48185 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 66f42d4061..a0031de173 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:5ce17afad842d4a3a2177c646a686ddbdfaf8d4013f65f74610b30acb7865079 -size 46465 +oid sha256:d6b27f6cf42ae88bb10a519096a3a6e469d6447a50996eeb61eb02b3bdbbfa26 +size 49895 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 5c4bbbb0f3..874aa52144 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:b4a23bbb8fcac3c6a76ca3c4d9d966d49d07d3822a6a35706ea2a6d4bf47f78c -size 37527 +oid sha256:f73fa94cd961c62d08d0abf046560c780fcadbf0ac412693e741fde8d093a3ee +size 40690 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 72e63d1005..8a3190e8a6 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:e06a1b371ea46d2c496c3a6bf792a41f4d4d01cc27cda204574020b483d2b94d -size 39676 +oid sha256:bb7aae1df2a4cbfbda74c6c8812198140e54f12b7798af9a1a90500616b05595 +size 42653 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 5c4bbbb0f3..874aa52144 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:b4a23bbb8fcac3c6a76ca3c4d9d966d49d07d3822a6a35706ea2a6d4bf47f78c -size 37527 +oid sha256:f73fa94cd961c62d08d0abf046560c780fcadbf0ac412693e741fde8d093a3ee +size 40690 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 72e63d1005..8a3190e8a6 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:e06a1b371ea46d2c496c3a6bf792a41f4d4d01cc27cda204574020b483d2b94d -size 39676 +oid sha256:bb7aae1df2a4cbfbda74c6c8812198140e54f12b7798af9a1a90500616b05595 +size 42653 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 9d04c2dadc..62440e7612 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:0e5a70c38ed47672d570d9afdd3b8e4b188a9ab90cd2983299c10c7f765a3582 -size 44232 +oid sha256:e0ae03eeb528590d37def020f34e8ab138e16be7118c8742ffb5e5b0401ca91c +size 48185 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 f0fd0f9911..25b2d9076e 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:1a98b68616d52611d2e2a83d004e8ba3a7d4d015fcd872368963d37c560b3e69 -size 45325 +oid sha256:5df79822c29afc140d67075460ebb3ac32acbb12bd48b422cddf2b7f30f0d5a7 +size 38677 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 84de6ed493..a1f7290ef4 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:1c58e3c2bd157daa0f678e2d17f83c790497167ddae8409c4b3d38a41f25cf7a -size 44036 +oid sha256:2d5ad508b5d73947fa07f9d5f56ec49949bd4efb60ff239df245814e26925a60 +size 35579 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 e4c5681b75..75478ce4ad 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:612c429945aa9aed4eedf0607c920887e4971d1a278dda03ace42ceff38f5b3b -size 52022 +oid sha256:3d73f8dcebed2575d4de00ebf9b44e19e46fabed0fd9b252c5df6f5c4e82753d +size 47314 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 167985143b..d48bbf0afd 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:7853b6076610c5812c08cc4a615a7baa74b2ce379d8eef255bfb7e9f1fda58c4 -size 45253 +oid sha256:02d9b9d5219a47dfd74a523f3e3b38e6273df6681686b086fef15187cde101d6 +size 38608 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 165d6cd269..2f7500dd52 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:079e6ed6cb2a69087501636ef9fd881e075a2ee49d177da749856afaacf82b38 -size 41730 +oid sha256:be91a4e37639900c7d822b7922ebd3304e0cd6f33d69a8dc8797fbc898389f30 +size 36516 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 55530ba18d..68d5d0381f 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:de2f0279925a81865c79599b1e4f8677357a28c9b3612098f0be5d84c5fc5051 -size 43003 +oid sha256:f6b9b30eecec15ac0adf1ba18dc578553770e228c944d928dc87bb308ad66a95 +size 38846 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 d9130c5ebc..e80ac211bd 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:5709475d9bbcb55d7db83d856d74c4b1141f4f4904ee228652475afa43d67009 -size 52505 +oid sha256:630a97f988f05420ac8348c9f5a446970ce1c823b7157772b6a1294410a0a555 +size 48100 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 bdeceec6ae..391ef68dc9 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:82e8e9e219eb8360e8ae8b9a15a92a78898be0594321d92845da39557bb81074 -size 49990 +oid sha256:199d550dcc10ad32149be1e99aa8da10b0bf13f072e54a8e809e050f8cff2335 +size 48598 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 212a5ddf32..1a36865c33 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:26471123c92b279454a8c1654f52625344a1ab67e83e9eeb92640009f1047a0b -size 50338 +oid sha256:d4973fd90ead80ac942ed9c7209ed309485d43b87cbd1ec89e4f0989f2a14e4c +size 50550 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 8ac217ea46..c64083082c 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:c99235e9537f161d0581d91ea5d081ffdf7321a3b5a5f73a3c146db66ef3fe12 -size 64150 +oid sha256:2c95fe5ff025b2690fcf4466ece56f41f0758e84004a41d00fda090c5bec0811 +size 63879 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 36672b51dc..113372b8b1 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:b64c6ec76e434f18348f9f8cf9166bac55b3a222cb987d1315dab415cd4b3605 -size 45725 +oid sha256:d52e4e8ebb9f92f23445be1374cb1c99a55ca4bc855566f7eca08e31ec419ee4 +size 45862 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 b7a1cbf269..2540918855 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:a23784d588e4ef38ff320b8c1c79181488bba247510f7941f7ec0469a0b6ba66 -size 43790 +oid sha256:638d958fa225877f475feb357c0d839fef8a05b876238522c9750a8b90b23358 +size 43922 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 7379c8d351..a61d4381bc 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:4edb9c0fd6de708bf802c7deab1204738eb73e832cee8d76c7298bc55bf9e2b5 -size 56883 +oid sha256:89d4726d9deb408e2a7fac1a71ad487b154faa9735131bd1fcbd64d0587746bc +size 56525 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 36672b51dc..113372b8b1 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:b64c6ec76e434f18348f9f8cf9166bac55b3a222cb987d1315dab415cd4b3605 -size 45725 +oid sha256:d52e4e8ebb9f92f23445be1374cb1c99a55ca4bc855566f7eca08e31ec419ee4 +size 45862 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 a8c2c353c4..23571dc46c 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:57ed78f6e03fc4ad8301943c45a1c16c51186c7d7b24d41070565cfaaedb58dd -size 43223 +oid sha256:c60eaa6e529c230303960d0fa3ca03eec6c010bef06bb6a46db2d1241472a312 +size 43377 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_8_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_8_de.png index 3b870e4110..3ef980974f 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_8_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42c4e166849a1699d265bfecefdcf3bf171c3d76632739e6976c60298722ae8f -size 46557 +oid sha256:20c92b5c171e00018f7e5ae66e55706d2c264eae4d76db727241a5978d0b8d99 +size 46763 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_0_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_0_de.png index 9fac418c7a..295c6141f3 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_0_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac72afa607a14632318eb34a88adbd46c5417d55f86cd3e87f11f121063590db -size 29434 +oid sha256:b17bdc28601b61df525fd8375dd83d79ef8963dc03a436979fba88564fb12318 +size 26476 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 9ed5e15b37..cf5a3e7dc4 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:edbe32291aa1db0858a5b681fc8f8c14263c1702f1fd734d8dfa67a8c25555ab -size 38174 +oid sha256:41607b403d6a0cf790db112ef3908df6f02ce1a2013647f667af8a539221888d +size 31781 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 e83690b0be..7d68ce8154 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:62a4c11f1d9678ccda6ea3526836aeebc50500f5284010af2bf463feaf738838 -size 34034 +oid sha256:c0c1d9181d312dc01cceedd9fb446e7c4e704b20dad2eb00eb933dc0474167cd +size 32512 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_3_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_3_de.png index 9a956782b2..bffec9be0b 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_3_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15ce268f3519e116352386721e3e822a363da3068655642d776696df443f45b9 -size 29154 +oid sha256:c404ee0951b8f5102e6c4be33818eedde2b74c0dad4e9bcc3d1680a21ddba208 +size 26378 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_4_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_4_de.png index eda09521fb..855fe194c6 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_4_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c1678e1b006539b426fa43ed1727726aaf5eff762161740e162d6a9838a86c5 -size 31033 +oid sha256:0a840f0965ec9cf1a0bb10e0b6421682388ec9153489b1306ba21e34de67cab9 +size 28445 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 f9cd2a3c19..4cfdd1a2df 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:899f3eded3c1635771414862c1f8331e66c5bbf7d3ea28feef98962e5a41d382 -size 52659 +oid sha256:10c9b3b2b60263b77aeac6a0a726200d82c89fb8ef3f9fc1c5d67eaf82748c0d +size 49097 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 77714be81b..7ddf22af02 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:a2f90e632555ce3997552928c34e2cc3b0d56cef010cbc18b47eadfb3a5e191b -size 69783 +oid sha256:e7af5e98b0b15551c1c91fff4e5333dfdda2b0edc2b2c94b69a30ead3957f332 +size 66780 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 0d9a692f2a..f2a212c869 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:a7aacc61e3c37564d3f32df8c8128e4ab448ca49b16d6e9cc25f5c95a963cd02 -size 69902 +oid sha256:e0e0bca9a56eae0d774652862621c1dc33feb1dfae88f633726c0c795a1d2427 +size 66905 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 d97706fbd3..7de7d8ce2d 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:d2ab8002721017aefc3e9e254d643cb80b380d14098b73bcf3cc764b4091290b -size 69281 +oid sha256:57cd6df19cc7b61a1c528e7ed107f8f6006d8a9cf1b82946fd45acb78746644e +size 67046 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 1b8c7098d9..f36a6399ed 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:fbd977aa083cd3fe3c39abd79f7966aadb20f6de2324207d6ec19c2904ebe23e -size 69079 +oid sha256:921f770410cbb2d168bf8b1ad7c3297208d7ca7d8d610ab30b4b65ffbdb9897d +size 66728 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 d5d8b4cc79..e3f4fe168a 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:b178fafb154da8c221366c5f569c60d0c6ecd7428c53a5bbf941519447ef97fe -size 52421 +oid sha256:19d8a60cf3c51f4ac26afd9dc8971d747c0ccf7ba49b3e8aedc3f06db54f6d2c +size 48860 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 d5d8b4cc79..e3f4fe168a 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:b178fafb154da8c221366c5f569c60d0c6ecd7428c53a5bbf941519447ef97fe -size 52421 +oid sha256:19d8a60cf3c51f4ac26afd9dc8971d747c0ccf7ba49b3e8aedc3f06db54f6d2c +size 48860 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 cc41582f26..c8d111b76e 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:22f2e704b8ceee4d52fcdaf2d62492b65cce83123b9f462720838e1fddc98bcb -size 54078 +oid sha256:d685380e4b4ec9634ceab25bb989dda305000bbc63d388d308584ce9be41e900 +size 50034 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 ddf4401141..da38ec710f 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:d1f2ee196f379b8c27f0c5e908ec9ef64cd034b7ed992dc7d31aa9cace5c2b39 -size 45958 +oid sha256:1711578b347285b594410d9c5bba9c5631fc930e45ccb78b25820b5f02a7a52f +size 42777 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 69625cdc1f..21b2757a42 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:b38ab8cec2863d4db7bcdd2e77efec75dbd5360872f66f5fc6f0c6a181ac196b -size 55062 +oid sha256:6d11b17b52172a2d8b98acb81c0659dc97d09ee75ab232557a8622bbcbde22c0 +size 51007 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 1cc6edec3f..1bc76bf222 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:ea30884a56e97630c890f29b19ce655d2f88e4b1d9f2f123ab4f3295c1455c8a -size 72594 +oid sha256:ec5fc09759b68d94af3e9cb9b8a049b4b09f9c5d2088014021bf662f45a89d03 +size 69361 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 9fc37cfe1c..9fcdbe91b4 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:6d682879e6297b497e36ae688ee301685ce6b584599bcc399e19f987103efbed -size 72826 +oid sha256:016bfc7341d5a7811d02a3343b3d05caaf9c822b3a3d773385a7b4777644bf6f +size 69581 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 0781b1c63c..403b3e6875 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:3c614f14062911a099cce2739490c2dbf6ab9e604d1ae4c168002eb219cc1f5a -size 71863 +oid sha256:e16d6e7dac990919c01f3a50dbad23de8122e9fce0f9ecb01cf392d2013dc037 +size 69597 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 259646f838..566b4a29c2 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:75448b498fd54d100286b9c716b2a429ab2f1d5e5ea45fd28502a776ea0d8ab7 -size 71795 +oid sha256:40e3e22e21b260c716f5d03a8630c9acfb8d84ee054fc996e952c71076682afb +size 69241 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 b0389f4100..4630cddbc0 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:6fc6be7296f4b3bdc1c6f4ef61278212f290b18f4404bc7244018a0e9b6c5a01 -size 54865 +oid sha256:38cc47045d968a748f6b7abd6b4415b1df9daeae23ae3ab00cf6c9850be4e025 +size 50973 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 b0389f4100..4630cddbc0 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:6fc6be7296f4b3bdc1c6f4ef61278212f290b18f4404bc7244018a0e9b6c5a01 -size 54865 +oid sha256:38cc47045d968a748f6b7abd6b4415b1df9daeae23ae3ab00cf6c9850be4e025 +size 50973 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 3d5c5b68ee..63c9d2f2ec 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:fb24bc707910c1dc60679efe310840c62f8f35337db9ff846d491272e603ccc4 -size 56618 +oid sha256:9322e1e84b86d8deeb0ff07ce296847c54d620901fa072fbe5321c1197fac16e +size 52370 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 bb751c8bc0..afa28c596b 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:64a5e97d73f2bca93918916af241c2f19f9a9f1281fbf7d8d4ce91b9efdf6b3f -size 47583 +oid sha256:cc795edca25781362cab6a56ba73dd3ebde43544a82c94711b04add0fcde6760 +size 44368 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png index 43d4a8fdba..aa301e7388 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:53e26f4b4e9badc0d9ee81b34eb39e5983c1711341ff39c5a2501a5a7b9cbef9 -size 46488 +oid sha256:157e55e65bc769b489076156cd387185857480b1da354303fab02084b2475085 +size 46422 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png index e3af8fe59e..fccdf986ff 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:01885455bc41f2d5e2a6579d9f8cc89dccf9c20423a840ba7bdb135b2e9946ed -size 45365 +oid sha256:57c333bda4ed242cc088ed897b10af0e46ddcb02a15d2703b99b3ed4c8655f65 +size 45292 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png index 07cd9cffe7..91a19d8b15 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:533f8becc79f0f9bbf8a8e0339d200ce820cb24736a53c469ece0bc220e7c524 -size 47523 +oid sha256:1e1b2535d2ba22a2fbdcfdfd2af2922fb823cf48e5543597ae495157d7c93de2 +size 47448 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png index dd98477d4d..b01e41e9bf 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:d5d6b9d043362c56c019261f193813a5d0401ff77a56c8c7123514f99e98841c -size 45583 +oid sha256:c21465a9222bd9c1e8ef4cd87dfca89f58a680a1159a41de59c5f235dad600d0 +size 45520 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_14_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_14_de.png index 59e6b50bac..c8b623827d 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:f5211fc81304fa414a6ffbccb9a9acaa6e68fae9be3e4d49faf801046e6831be -size 46169 +oid sha256:2de1957ccfe09b93100ed61addcb8274d6f20c87a28759d11ab02ba8a1638804 +size 46104 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_15_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_15_de.png index 8112704fca..dd58719400 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:825835aa51124aab98c45182c93bd85deae427d7a7f484211720948523e7e599 -size 46703 +oid sha256:7bbb4123dad65552f7a5ccd998b95c5adc2a31ac7102f3c6254f8eebb8d12db0 +size 46638 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png index 54993dfb57..666d8d0c47 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e54b8ba8b476ef5b03f45c647364c66c88d1ae95c99b9060158a277cc1e6648 -size 45930 +oid sha256:1124f60a8b3018c53e1f4a5ab822e49fb6224859901f49957a61462e00f62a36 +size 45864 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png index 85778227bb..624318885c 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d3fe57dac71d0fdab6cd1027f35367de15a6f432e4c7c31e127bab26e05ad60 -size 45178 +oid sha256:df9db0d4e17dedc02ff65dfced875fd454e8648b44147a059ae77da7fbffac86 +size 45099 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png index 05051a942d..9e5cf2c548 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:23ac17a20e192be59d41bfb0ba1b307558b9f4b32d90b3f23482ddbcea4ae95e -size 42568 +oid sha256:8cf15623f94fb77ece859777a5d9f874e9e799afe61a181ebf7c231e198d9d6c +size 41991 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png index 427a786440..a1fda78fee 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:8ce32cad2d400336e1b29675ea9e8a16532efa5be54009f3c678a00ab5aa6de6 -size 40191 +oid sha256:f7871c2de6d77425ba0ea7d773328a2a3272594b35a8602a01054fc2db1c1051 +size 39609 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png index 8ea9cae1f8..5e8dc32fce 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:6462fafab220a93a72e7e258a0f2654d8853c54849e8497d84e2247f3f576d50 -size 45881 +oid sha256:a867633de36bd2967ad0b2049cccfa3edef59f3eb732478be8bbdb03c6198a69 +size 45734 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png index bd4c90331d..64e13fd312 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:11b866d4f836aac78ac237d9d61406a55ac854241af8f3fb3eea9ad54af400fe -size 45371 +oid sha256:01791e192c3cc7429544e4928e696a90b6a32a584dad5fedea8301f37b31ad79 +size 44840 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png index 7148b85dc9..3c026d6c15 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:eb6a6bfd308fd385e2cca8e99bea65de49d8ea27986de920989035bfe74d2449 -size 46599 +oid sha256:6967b9dbd4f79556952c527985e4cdab7c569253716a5d62eda24d46b33a703f +size 46516 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png index 8e7c130464..72aadc6460 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:1ef2e551efb9b22cbe17dc857fac440c33c6dd36e2ef1dce329aa1606d92e67b -size 45639 +oid sha256:51ff86136abb6f6d76ce6f51978aebd4d710fe7f1b753a7782146e28b5656d7a +size 45573 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png index adde43541c..8138af4888 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:f0c38801d4ce5d5040af956b7c96f795f5251892bad5a9331858875ad5b5a048 -size 44679 +oid sha256:3e338bc62994b247477cc169c0bcba1f8d428b590f2bba1966136f52ee7b7441 +size 44529 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png index 5e240e59ff..104a5e4e1e 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:a3b28f06bec8d728eafbd7a46588ac931e289cf36336ae4680791dfca3dea271 -size 47583 +oid sha256:49a21b1f181bd32b9895763b045952332225e71dada7fc155203bf74b1419aa0 +size 47522 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png index 28a913a3bf..e4504b8123 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:a8856c7eadc1e675d48638494ec3b14c4606c7db197d070ccb9af0a5c8114d26 -size 46405 +oid sha256:4f588f9d3f51ba3ff764a9d4bb3b942e3ad396f939115f35da0a794a91b61aba +size 46342 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png index 6b897fff33..9b95c8d176 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:22e4fcfa18a93a0ed4369d7aef331150f8300ffa5356003896674636c9e0383b -size 48143 +oid sha256:f4ed50b1b0eaa7dbf2e050f4e1f82f46626a3b3bc986bf633fe3f5337a610c32 +size 48061 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png index aa223f8ada..792e6f22fd 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:0bb3c14f381583a5bc068cce244ff2105910cfa31791f7839a2951481159ec56 -size 46657 +oid sha256:3ffbe45a0a19c19ecc59259107b9f4acc58ed0b994abd0ac0603a7051d2188f4 +size 46596 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_14_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_14_de.png index 470f3817d2..bf121f9929 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:be8328fe5952d93470b538321da39b055dfb69719778827653b0cfcfe03a8d94 -size 47180 +oid sha256:5aa801587fef2a7131cd11e161969e028331be4b4a429a86d25bb3c777757144 +size 47119 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_15_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_15_de.png index 8be5ea31cc..5f1495fb1f 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:e74f30454cc09ff1c56c763defb683e21c6c86153d8d79e5a5dede025fe19dc4 -size 47748 +oid sha256:86b0e4d70d7dc9f5093e301ca1a7e5364a99c2c6f5608aaaf2fced230356faff +size 47703 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png index ac97c9cbee..67cb9400f5 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:229b37d061dbee966eb6421c4b3dd1804c624308b13407ba64219ca473cb4564 -size 47003 +oid sha256:f00897f7d6cf5c6a9d319bb69390cc10d4be20451850846b14521361f46c9267 +size 46942 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png index 5787e10f3c..903e2989b7 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd4ead2edbf54b0b91f92f948d8a68afaabd427a069458b127887ccfc337d915 -size 46490 +oid sha256:4aabc001f2f14832f71dc331bd53f001f67d4f2d248768e79f41708b46d671e4 +size 46431 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png index 25a3a06a41..ebe6a3d972 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:2cbeb5fcef8dc243ec1b5c457560a5b278b2fa7c9c7df86f91eccce8ac9eb7db -size 43809 +oid sha256:c1ff08b7776a8f9c772928d88028bb43da2854dbd140a16cae65b211bee05179 +size 43219 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png index 46b21d5a2a..c3b7fcf83a 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:b69d698c66368055776d69f6ccacb796cddbfa367c192cc2219e3d18e1c73656 -size 41328 +oid sha256:d102c08371deb5252914ef656b0645a071422af8dd4b425de9b542fa4d6593ec +size 40739 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png index e551eb69dc..680b42a906 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:6c55fd8e61f41612e91ce6cc27d7955c24f5c028aa4e161f560e14db4d7f7d77 -size 46841 +oid sha256:73839237353e443c199c56b67aa4d51b1cdb9b6d08707bf31cc3376546f97cfd +size 46673 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png index 59f7752723..55b3e290bd 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:8c5058e093b29b515028f268592ac7c47ba3fa9847f2e9791db25357bc61bcc7 -size 46430 +oid sha256:d32fb0f4761cded0b823b4e221b8d920cae8cae79e39895b91d1f861ef5f89b5 +size 45887 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png index d693d6d649..8ae0e7bf9b 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:fbbdaaccb534a1cfa4288dc7a019699387d329831802153b0f4aa9b9580a4584 -size 47850 +oid sha256:7decfaeb8bd1929d6232a5148b8b7a958eeacc1bf5baf779f6f633c44dfa53e8 +size 47787 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png index 9255958570..9096b37553 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:572ea075e5b270bb58f958c61e9a7b7e178a11ecd16ce9bba34fe0372c78adf5 -size 46767 +oid sha256:a212f79364028820a27448e6fe3849bbcf9a10a66d918eb347e546c65280bfca +size 46705 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png index 72450a3020..17d4a0a3c0 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:a9551167ec223ba7bed45858a85ceffce763e6808a7954ab6e09b088adacc3dc -size 45697 +oid sha256:58188951ba569cb9a9fafa778bbdf9f83f7a6d201975eb9c37c4f48473e8d331 +size 45567 diff --git a/screenshots/de/features.roomdirectory.impl.root_RoomDirectoryView_Day_0_de.png b/screenshots/de/features.roomdirectory.impl.root_RoomDirectoryView_Day_0_de.png index 9f3e20e5f3..eccb0ac039 100644 --- a/screenshots/de/features.roomdirectory.impl.root_RoomDirectoryView_Day_0_de.png +++ b/screenshots/de/features.roomdirectory.impl.root_RoomDirectoryView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5b39e0b58f9ed1db274285d0b95767c854e14bacc1a416e0c96286cda169218 -size 13568 +oid sha256:ad3b20348912fd6f6c3e4a8e68e2ea19a491f61239c59643b0b247cd5284ef9b +size 13545 diff --git a/screenshots/de/features.roomdirectory.impl.root_RoomDirectoryView_Day_1_de.png b/screenshots/de/features.roomdirectory.impl.root_RoomDirectoryView_Day_1_de.png index b45df5fcc5..84fd62c0f9 100644 --- a/screenshots/de/features.roomdirectory.impl.root_RoomDirectoryView_Day_1_de.png +++ b/screenshots/de/features.roomdirectory.impl.root_RoomDirectoryView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50c1ac010924a7bb1ee65c07c209c591c9da95633b7cbc0aa8236ab5b5190439 -size 28518 +oid sha256:5fb71ee98f44955ac96021846d6e0a9e6f8dc6fd0a1175e7ce974fe3adf95613 +size 28485 diff --git a/screenshots/de/features.roomdirectory.impl.root_RoomDirectoryView_Day_2_de.png b/screenshots/de/features.roomdirectory.impl.root_RoomDirectoryView_Day_2_de.png index f9d5c812ed..a332fab424 100644 --- a/screenshots/de/features.roomdirectory.impl.root_RoomDirectoryView_Day_2_de.png +++ b/screenshots/de/features.roomdirectory.impl.root_RoomDirectoryView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c549721f24defa576135627f88117f057168bf1b64354078296bce9dd6b241f5 -size 30146 +oid sha256:362aa58b91f2ca04960fa1342f76ba28e28ddb8721acf92bf63ef0099d49c695 +size 30106 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_de.png index 9332c16d79..5e828156fc 100644 --- a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_de.png +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d43dba57001db80c6b16334ca15ea2ae39df9c597a1adced3bb9b34f7c652163 -size 28922 +oid sha256:1937f15d45ff18da9b3203fe420aa6242114de82a9614e04eb99f5c36bc567a7 +size 28214 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_de.png index 6d56bb9a7f..f11c344ae6 100644 --- a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_de.png +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51fe1a74680a6975652e5939665f64d116246015ca94cd234dfa9574a0bbc426 -size 34192 +oid sha256:6dd52f4b17d3ff94b4d61db544fddfb8b449da49673ecb45a02fa297966b52f6 +size 31604 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_de.png index de55bea6fc..9a626deaab 100644 --- a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_de.png +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4555565484cde0f2beb81c26f14bc918f289f66cefc8d76b7b8dca0e29543d4d -size 34498 +oid sha256:5037873ed63f558691c06a63f93eca0d22dd2ec1f70aec2dcbf9481e73d16412 +size 32099 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 eb201eaf9b..2c946bc23e 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:6a23da15af668434e8b4980fdb9882325416cd8b02704a1a2198969a1ca30ec9 -size 73662 +oid sha256:e82a7124397546cb3c2293736b743cf2c5ef943ba010c6d7b73b2d70a368a2ac +size 75425 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 eb201eaf9b..2c946bc23e 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:6a23da15af668434e8b4980fdb9882325416cd8b02704a1a2198969a1ca30ec9 -size 73662 +oid sha256:e82a7124397546cb3c2293736b743cf2c5ef943ba010c6d7b73b2d70a368a2ac +size 75425 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 d5abdc1cfa..6912ac7984 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:86c4d6448e42855bc8ff4f2f6607a597c5d5eb9040be750596d21bec67cd32bd -size 74288 +oid sha256:9e43d692e1cd54898f38019f50c0d36b9575d4c2db534d898999eb9ed167bf26 +size 76051 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 f86ea13aaf..ff25ff9a77 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:a908b43102585b666bd495fef2f392ce32ebcf3bf05c62e2a38217ab63d4663b -size 47047 +oid sha256:e3f99c41bdd09088037a6fc0e7436f71867e679e2d4f829156cf774cf5c06ec8 +size 46051 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 d7b18510b8..f1c7d5fd03 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:87376ec236162d94be973568e748f130e06919e32fffa0a2dc58bc6a63d54d97 -size 45603 +oid sha256:82b6921b8e15336319f4ccc2a9f30f4e0f0bb8aac01bea84f2229d12eab87f22 +size 44547 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 59a08eeebf..b86d0f8ff7 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:57fdd01154ee9c6b47064df0876116e6e748189ba43620360bbd56a0e5c343c7 -size 54891 +oid sha256:6041785efd5e5f3551639d23ff6810169855efbc6de52343ff7bbdd5afc50db0 +size 53964 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 37cdb90a65..bec4d30b3f 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:5faa2b8d80dd59a1d50e39f42219e07c8b1d353e2b46ab6905f608a40734705a -size 54936 +oid sha256:78438eb23e545b1ed8321502562eec75a720f8161ec96b80a45d2cf37e999a57 +size 53878 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 fe1869e6e5..c2cfc1532c 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:3d6dd613650135a44946be8b74b380782059ff20b81f87b0af5061a197a60e4c -size 52008 +oid sha256:05096ace3fae4368464a78816164680ebecf5a65b03eb9bc6fc6eb06a0a43d81 +size 50930 diff --git a/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_de.png b/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_de.png index 2c19dce480..7e90bb8d4c 100644 --- a/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_de.png +++ b/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3dfdef8db439460bc1221a1a1bbc63f17c1dd28d4057b6d664fc9ab5eff5719 -size 48401 +oid sha256:eddffdae8d6b515a4140ad716e52c965b082f0fb1425aed9846c9d9ca35334eb +size 47357 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 b1ffc24c87..21c587b71d 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:fa7a5ccf8389e9f95d2c8073fe67a13b3326f3bbd936d5a695697f4e038365c0 -size 31209 +oid sha256:ce26976414de9975b52aab806a8ff3de282c9ae7e51999b5071d5e34be0c08bb +size 30651 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 e5820c0553..f246e38806 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:e34410a9c8cd9101e1ee7ed325c5bd0fb9caef604d8b36c86a9640806ffec0d5 -size 30072 +oid sha256:ba0700dea1c4dc744101290e093b205dc3f24b1dd0de8ac2ee14103d636b0d70 +size 29851 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 e5820c0553..f246e38806 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:e34410a9c8cd9101e1ee7ed325c5bd0fb9caef604d8b36c86a9640806ffec0d5 -size 30072 +oid sha256:ba0700dea1c4dc744101290e093b205dc3f24b1dd0de8ac2ee14103d636b0d70 +size 29851 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 9acaa7a56d..2922220aac 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:a9e9bd98267d2a9cf39bf78796cd330e2dedf674f5eb67d410d7527847b6f3ed -size 45297 +oid sha256:c710500bd2320a6df97c5d78d6fcf337e3ddd21115a2acc81e1de1fa477121e3 +size 44689 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 1e75ce513e..f97909d2f8 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:f443fec4459c6af35efec0a3bbbc10737e3295456c17099c61060997fd3f2c07 -size 69576 +oid sha256:7aaffb9298241bf42c8b31806e039f20ef585f8ff51466b39ccf2bd19f7723f2 +size 70384 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 f7360421ca..45e32ffc99 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:da9bfa180a08787943a22b334f0aa7ff09f3229404581967fe27a26223f119de -size 53340 +oid sha256:a1468fd52b8e3448bd1f8e0f3791c574565adddeda13ea8a13b2332e132522d6 +size 50128 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_0_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_0_de.png index d405b610e6..e8832f5e11 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_0_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcfd2629be974eb03b0f6f41670f26599fa78ff4c779268568d68a0d23162954 -size 45139 +oid sha256:8d3d2644cf615ad50bc8e38dc58da5598e248827a27f73f47967a05a392b31e5 +size 43560 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_10_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_10_de.png index d405b610e6..e8832f5e11 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_10_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcfd2629be974eb03b0f6f41670f26599fa78ff4c779268568d68a0d23162954 -size 45139 +oid sha256:8d3d2644cf615ad50bc8e38dc58da5598e248827a27f73f47967a05a392b31e5 +size 43560 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_11_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_11_de.png index 9d322510f6..f4b594d1fe 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_11_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:912d5638eb392917cb1dc18e43dbf9e978bc9c5c1582a35a4af025a9b1127367 -size 46068 +oid sha256:5de424d81283d12e82c18a61ad3f3089181fecfaa0767e7bca93306e0b392c1d +size 44472 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_12_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_12_de.png index 9d322510f6..f4b594d1fe 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_12_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:912d5638eb392917cb1dc18e43dbf9e978bc9c5c1582a35a4af025a9b1127367 -size 46068 +oid sha256:5de424d81283d12e82c18a61ad3f3089181fecfaa0767e7bca93306e0b392c1d +size 44472 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_13_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_13_de.png index 55afec7620..b088028261 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_13_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_13_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60929d900781d15bc764ff3120289c876dbb0588a8945129fcadf548c27b38d8 -size 74613 +oid sha256:16caff51af4ff119b2497d40a3ccf28d6d5c042a905c089cabe956a4c7905ab9 +size 71290 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 f605d74d53..24aa5c3831 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:d06c4656ce197da1d76cd6067fe16b52c6ae2bd621bc6ee5804b0f3e9cbd4530 -size 68142 +oid sha256:f32adae89e7356af6fa8396774184617a71e3ada252662ee4b993c29dc46f70d +size 67033 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_15_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_15_de.png index a19b5967e8..8617b520b3 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_15_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_15_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7acde35b0674305b200b96226e5c0eb355bd89a62962315349a9cc026fa3eb14 -size 58630 +oid sha256:23b489947d0f9f3ece1aa514d1e74d9a2c355177052b48ea02a7d518955cd5a5 +size 57075 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_16_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_16_de.png index fa51cc3136..a3036d76b3 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_16_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_16_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eaf76022e22435ed2a6c4d210c96e24b529802b84d077b6e50812525fe6d68ee -size 72621 +oid sha256:47fcb7163ca9930ef299fe028821ae10f4d25baf2e09e8ed49a14a39c47738c0 +size 69177 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_17_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_17_de.png index 982f8daeb5..92c9ac1578 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_17_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_17_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ede603b925fbbfe304fc67e79e96883c3d5023db775b53e4f79da9a3a1e019ca -size 49095 +oid sha256:c60f76b4095bfab8c28ac4889af6b020d4d82eb73e2153d404a1f1229fe9c50c +size 53093 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_1_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_1_de.png index 9d322510f6..f4b594d1fe 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_1_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:912d5638eb392917cb1dc18e43dbf9e978bc9c5c1582a35a4af025a9b1127367 -size 46068 +oid sha256:5de424d81283d12e82c18a61ad3f3089181fecfaa0767e7bca93306e0b392c1d +size 44472 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_2_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_2_de.png index e68d862bd7..3fc9c08e60 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_2_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65a9cdf5c00daaac7cf3bf3130f8f9042fa0418f0fed7d207989d07ceb212d5d -size 46444 +oid sha256:54a67ca1f96e212760451beca08da149f5dd13d75a4fb5084088e09efb94f332 +size 44832 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 1486e3bf5b..cab4f598f3 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:c179e94af188eb4e0496e8cfd7422c59cd373d5becb21782d02fb304ab6ce158 -size 47159 +oid sha256:ee4b0794963906b731f4991b23eeca85b2f19fc90634ab4917cb59d4fa65d8ad +size 45555 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_4_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_4_de.png index d405b610e6..e8832f5e11 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_4_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcfd2629be974eb03b0f6f41670f26599fa78ff4c779268568d68a0d23162954 -size 45139 +oid sha256:8d3d2644cf615ad50bc8e38dc58da5598e248827a27f73f47967a05a392b31e5 +size 43560 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_5_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_5_de.png index 9d322510f6..f4b594d1fe 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_5_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:912d5638eb392917cb1dc18e43dbf9e978bc9c5c1582a35a4af025a9b1127367 -size 46068 +oid sha256:5de424d81283d12e82c18a61ad3f3089181fecfaa0767e7bca93306e0b392c1d +size 44472 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_6_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_6_de.png index 633c56ca7f..f36902ec07 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_6_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:440258bb1f0eb7ee8e866f8cf9b278869cb35913e62f7883a4b0cc83753a8742 -size 38805 +oid sha256:818f5f1d73b601d026f513387d76e67628142609412c0afa055d530be94db263 +size 42157 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_7_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_7_de.png index 9d322510f6..f4b594d1fe 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_7_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:912d5638eb392917cb1dc18e43dbf9e978bc9c5c1582a35a4af025a9b1127367 -size 46068 +oid sha256:5de424d81283d12e82c18a61ad3f3089181fecfaa0767e7bca93306e0b392c1d +size 44472 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_8_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_8_de.png index 9d322510f6..f4b594d1fe 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_8_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:912d5638eb392917cb1dc18e43dbf9e978bc9c5c1582a35a4af025a9b1127367 -size 46068 +oid sha256:5de424d81283d12e82c18a61ad3f3089181fecfaa0767e7bca93306e0b392c1d +size 44472 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_9_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_9_de.png index 9d322510f6..f4b594d1fe 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_9_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:912d5638eb392917cb1dc18e43dbf9e978bc9c5c1582a35a4af025a9b1127367 -size 46068 +oid sha256:5de424d81283d12e82c18a61ad3f3089181fecfaa0767e7bca93306e0b392c1d +size 44472 diff --git a/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_de.png b/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_de.png index 046fdf98f7..0678026c29 100644 --- a/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_de.png +++ b/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dfccf862e7d77889f282a7f2b5bc786ddd1cd3727c3d599643067ac37ec4bb34 -size 20722 +oid sha256:03a0223aecd5be2b4fae6db8a30ef4ef61575ea0a4880e8f7711bc671819c307 +size 19212 diff --git a/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_de.png b/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_de.png index 0ffd345800..f9a58be4ac 100644 --- a/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_de.png +++ b/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fc2023ee98a0e4522033d1e3309cd83905a06ec26dfb094cbd37c99e8b4011f -size 16941 +oid sha256:f7a1fc3cfa5131e4b88f79969422635860d10382dca3dec4517512d1c7320de6 +size 15394 diff --git a/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_de.png b/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_de.png index 046fdf98f7..0678026c29 100644 --- a/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_de.png +++ b/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dfccf862e7d77889f282a7f2b5bc786ddd1cd3727c3d599643067ac37ec4bb34 -size 20722 +oid sha256:03a0223aecd5be2b4fae6db8a30ef4ef61575ea0a4880e8f7711bc671819c307 +size 19212 diff --git a/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_de.png b/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_de.png index 0ffd345800..f9a58be4ac 100644 --- a/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_de.png +++ b/screenshots/de/features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fc2023ee98a0e4522033d1e3309cd83905a06ec26dfb094cbd37c99e8b4011f -size 16941 +oid sha256:f7a1fc3cfa5131e4b88f79969422635860d10382dca3dec4517512d1c7320de6 +size 15394 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 3d64b914fe..22a3dd4e1b 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:41efb25b3a2878ed5f1da9e965541df89a2221dc76826b02d6f5e451eaa55108 -size 51976 +oid sha256:01ba9ac649bf68d555d5d840428fee0266584b96ce5fe36c0f9e2e1d6d0c4de1 +size 51074 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 2aec5827b0..48b168dc44 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:036a9f3744673cd2af05fd1b87c38e87b11d9df3748bb812ed2bf4dd3ea534df -size 48505 +oid sha256:fcd83326c3c7f7b43243e904197e88e21bb8f128bdfbdce722e4b7daf6c9a31c +size 47543 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 b9dcbb7810..6796840a99 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:183d775575bddcb0fab1b15363cc960914912dd43997469f28abf418dc85620a -size 61176 +oid sha256:5cbfd2e861308c906dbb5df7949d10be226b6f2a7eecccd6387a1d2902b235c7 +size 63562 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 b9dcbb7810..6796840a99 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:183d775575bddcb0fab1b15363cc960914912dd43997469f28abf418dc85620a -size 61176 +oid sha256:5cbfd2e861308c906dbb5df7949d10be226b6f2a7eecccd6387a1d2902b235c7 +size 63562 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 4f9799791e..d58496d0b3 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:fa05ada01146fa4cfacd1bea817959e7253c5fb933f8f17320ae5f2c8b0a9e83 -size 51846 +oid sha256:edc3adcd73285dbaf217485d9e296a1af1ef592d65baca1fcf6ed75700d17eb3 +size 51568 diff --git a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_de.png b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_de.png index 1a226e1546..66bc4a6c25 100644 --- a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_de.png +++ b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb52202cc8e4a53de3556a6920817cff3f6df930824c5c58d2d4d8f30bbf85c1 -size 42661 +oid sha256:b3d720a5a7bd70867d8096a06b70be72d612f86ad8cbcdfa5521a2c33cee0235 +size 40595 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 f2c5fae8f6..cfd34858d6 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:597162d2e0f3d1113e0c1dc75ca8b9081bfa8c107499bb9c952d4b4c481a20fc -size 61462 +oid sha256:c97aeaff0cd23f76604dcb60ae08ab50a48ef8249e83592327b84f8aefab0a52 +size 63490 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 62c7d701f3..557a86b152 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:c4be1c4435ac8ccc7a4d5591374185c1d96771fd6e11a27392d6d2cd80bb1c0b -size 57688 +oid sha256:6b3faa64a17045745be5e724be01eaad17f3f73d01b9fd6eeafa04a3b55c983c +size 60079 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 b9dcbb7810..6796840a99 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:183d775575bddcb0fab1b15363cc960914912dd43997469f28abf418dc85620a -size 61176 +oid sha256:5cbfd2e861308c906dbb5df7949d10be226b6f2a7eecccd6387a1d2902b235c7 +size 63562 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 b9dcbb7810..6796840a99 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:183d775575bddcb0fab1b15363cc960914912dd43997469f28abf418dc85620a -size 61176 +oid sha256:5cbfd2e861308c906dbb5df7949d10be226b6f2a7eecccd6387a1d2902b235c7 +size 63562 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 4f9799791e..d58496d0b3 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:fa05ada01146fa4cfacd1bea817959e7253c5fb933f8f17320ae5f2c8b0a9e83 -size 51846 +oid sha256:edc3adcd73285dbaf217485d9e296a1af1ef592d65baca1fcf6ed75700d17eb3 +size 51568 diff --git a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_de.png b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_de.png index d0a8e3c4df..94bb8f0af8 100644 --- a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_de.png +++ b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d82ac18cfb6823b04e0795d574b09e78164d5a7b9a7e78773e03deb9effcbf99 -size 43953 +oid sha256:e6498b8afc9307a9276a8b767dd5b7275748c19da68827d034111f2a4690e7e7 +size 42653 diff --git a/screenshots/de/features.share.impl_ShareView_Day_3_de.png b/screenshots/de/features.share.impl_ShareView_Day_3_de.png index 415bf08603..9f006b50ca 100644 --- a/screenshots/de/features.share.impl_ShareView_Day_3_de.png +++ b/screenshots/de/features.share.impl_ShareView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8077b421a93fb66d11503d75b558acda35947f5886c0f11d6b81dcf1c5e7653 +oid sha256:384eb7f0964c98a0f9f78085e7523086818f76c1d56e6750424cff515971e357 size 8681 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 171b9bd150..86d209de85 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:7ceca6380333b385e58bcab925625270901033de24bcce391ba6321e6d880ac0 -size 63371 +oid sha256:fcd03c7d8135700d4253d498fbd6b72a6ddde58aefef8bff60b162e44acfb530 +size 61373 diff --git a/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_0_de.png b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_0_de.png new file mode 100644 index 0000000000..fc5a3aca3c --- /dev/null +++ b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d094a27accfb3c562af33e4b510d3bc5ff9aef94799573d30354dddb411d920 +size 15583 diff --git a/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_1_de.png b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_1_de.png new file mode 100644 index 0000000000..388998b224 --- /dev/null +++ b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49190b524bbf7e2c489b882ced36d26fcf7019fbedbcefec635a6757316e334e +size 18302 diff --git a/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_2_de.png b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_2_de.png new file mode 100644 index 0000000000..6a17df2a16 --- /dev/null +++ b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3686336e9bac54a626c0a2cf704d460e695a79f56925a631018a1d7e3c3a4d97 +size 45967 diff --git a/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_3_de.png b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_3_de.png new file mode 100644 index 0000000000..19bbf7cd4c --- /dev/null +++ b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbe09f5baf21e50542e9eb1e889630419d3495247416d8698dca43c6427e3573 +size 45916 diff --git a/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_4_de.png b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_4_de.png new file mode 100644 index 0000000000..97bb338e38 --- /dev/null +++ b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71486e65a7010fb74dc43c7a7e013e0a9c2fd38750f77410a4ba67232b50fce7 +size 38942 diff --git a/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_5_de.png b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_5_de.png new file mode 100644 index 0000000000..b8a80e79db --- /dev/null +++ b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b58ce117dde4af11c16ddf460b87c2d1030631ef1038ee6f5824d017e75869fe +size 45568 diff --git a/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_6_de.png b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_6_de.png new file mode 100644 index 0000000000..cb1c9985fb --- /dev/null +++ b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2844a4696db06085edcce66b8c4de5c89d2093ec4df216b09424b99517ebe1d6 +size 41043 diff --git a/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_7_de.png b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_7_de.png new file mode 100644 index 0000000000..b1797a1489 --- /dev/null +++ b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_7_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7723d8a70239909f1f11e3a02b5031d432454acc67b171e0ece591abb831fbb5 +size 35685 diff --git a/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_8_de.png b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_8_de.png new file mode 100644 index 0000000000..ff09556f6d --- /dev/null +++ b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_8_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b39e08d2579a63b5eb7a6cb9eb9c1c0ef5a737d017419994712a50c6d74f732a +size 15510 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_1_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_1_de.png new file mode 100644 index 0000000000..f02aa4a911 --- /dev/null +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c49521f9dc7177e565aad8ad5dede3a96c2205e7138ad97661db9bf26c38b29 +size 21606 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_2_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_2_de.png new file mode 100644 index 0000000000..551d14618c --- /dev/null +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8757b8898722de0ea2537ccc03c29d7b821842876b798a7bfd0005ef404538a9 +size 47232 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_3_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_3_de.png new file mode 100644 index 0000000000..cd7399152e --- /dev/null +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:856a18b6f0b4a2098b81013617b479e94c4bac5f356ae8ead9032a2ade653a11 +size 45947 diff --git a/screenshots/de/features.startchat.impl.components_SearchMultipleUsersResultItem_de.png b/screenshots/de/features.startchat.impl.components_SearchMultipleUsersResultItem_de.png index c461340be0..c1cf8833e4 100644 --- a/screenshots/de/features.startchat.impl.components_SearchMultipleUsersResultItem_de.png +++ b/screenshots/de/features.startchat.impl.components_SearchMultipleUsersResultItem_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1769c488634b825df5cd87973227b8957ea62c0edbd23b201bdc6e630488872 -size 100763 +oid sha256:3cc5e887a1de69fc216a215680104098f1a689e5db992c3e65e5b66b0891d1cc +size 101103 diff --git a/screenshots/de/features.startchat.impl.components_UserListView_Day_1_de.png b/screenshots/de/features.startchat.impl.components_UserListView_Day_1_de.png index e16fe590d1..aaa5964462 100644 --- a/screenshots/de/features.startchat.impl.components_UserListView_Day_1_de.png +++ b/screenshots/de/features.startchat.impl.components_UserListView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb17dc3324af50bf3de4bade194e15d2e58776f09571df9ee9dd186b9020500c -size 20915 +oid sha256:ac691a431d544e0e4c33c62b51c86f26efb9088a00a05cb9d3151256e2290eed +size 21823 diff --git a/screenshots/de/features.startchat.impl.components_UserListView_Day_9_de.png b/screenshots/de/features.startchat.impl.components_UserListView_Day_9_de.png index d6b10c6ff8..34ff3e91f3 100644 --- a/screenshots/de/features.startchat.impl.components_UserListView_Day_9_de.png +++ b/screenshots/de/features.startchat.impl.components_UserListView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f214bb5284421fec3e888dbda5ba6c43378528b5fb5bbd80d866dd8b1ff04f3 -size 38017 +oid sha256:fcd3d8e40e5d9f23c941efa17c37d0473df47d7924d2f51c94022a6c08f559a8 +size 37181 diff --git a/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_de.png b/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_de.png index abef461fea..dfcf268c05 100644 --- a/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_de.png +++ b/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dfa67f7e6470d840b3ccba4e44cba9dceeefdcc0daee1c989055bb2397690566 -size 18295 +oid sha256:21cb4937ec1a9b665068bf6d98ed2085296b0b4ebbaa817206c57f07027e7bcd +size 18339 diff --git a/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_de.png b/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_de.png index 08b1dbe723..cb580922c4 100644 --- a/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_de.png +++ b/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a73ecdbe320b067c85c977c9d9ad649960c26ef9ce71b7080488cc6a4779b43 -size 18268 +oid sha256:fd6c596d5f059a9dc443c93479253dd9be6d34d00bb09de39d8baaf73608ce75 +size 18324 diff --git a/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_de.png b/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_de.png index f256b13970..ee46fd3751 100644 --- a/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_de.png +++ b/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b915b6fbe89255837a9cb61ea4465642fee00074c9d0bdcf2cba0f544e7b8c5 -size 18490 +oid sha256:09ab5d26a3b18d9bdd503cdd62d7fd767088e286c5f72479fb422e24e26f4824 +size 18520 diff --git a/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_de.png b/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_de.png index 7a37d92c1d..29d715f4a4 100644 --- a/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_de.png +++ b/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70856d165ce82eb2fec054f3c2be237733b0ed6b13a10c5eaa30f7cea7ad2b81 -size 21807 +oid sha256:1333ba093bf4b010f660fecfa1e576443ef42cde7d0f42b165099ba58a530dae +size 21849 diff --git a/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_de.png b/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_de.png index faf4394aaa..0ac0915508 100644 --- a/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_de.png +++ b/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3b644f28632f427d9f4109b4bb5fd0fb9814d617c8e3af6e4aafc7b80c9c1bd -size 21425 +oid sha256:ad688fd9d4fd288022ec67b97fc1a73ccc769f7f8e5ef8622726543cf460ac82 +size 21516 diff --git a/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_de.png b/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_de.png index b600fe75ad..e785272441 100644 --- a/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_de.png +++ b/screenshots/de/features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d1b1ad058509bedd48422f5d64ca313c00fd50d2458a3f2f961910b1c5e93be -size 21741 +oid sha256:25aa162ebc1ac9bb3cf21e07414a0d405e7043b1f5acae646e1e57c9a63bd4d9 +size 21787 diff --git a/screenshots/de/features.startchat.impl.root_StartChatView_Day_0_de.png b/screenshots/de/features.startchat.impl.root_StartChatView_Day_0_de.png index b450e243a6..41c7fc6d86 100644 --- a/screenshots/de/features.startchat.impl.root_StartChatView_Day_0_de.png +++ b/screenshots/de/features.startchat.impl.root_StartChatView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff886e4d75f5a079527252384d4a96d2ee18af5686c1b4ae45b6fb7e4b4cf390 -size 28090 +oid sha256:50fe94aba6e76e638cfa9b5ffb574c46ff1757a0547a5c8ead0b9179c457792b +size 28100 diff --git a/screenshots/de/features.startchat.impl.root_StartChatView_Day_2_de.png b/screenshots/de/features.startchat.impl.root_StartChatView_Day_2_de.png index cb7209b068..60aa2ae9c2 100644 --- a/screenshots/de/features.startchat.impl.root_StartChatView_Day_2_de.png +++ b/screenshots/de/features.startchat.impl.root_StartChatView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b070f5671759396846a0796cb94307a429724318a74277a388839e79cb5c5e4 -size 31873 +oid sha256:dbda304432217316c4cc028d98e6394619c9279a261a004466bd5ddf640f994f +size 31853 diff --git a/screenshots/de/features.startchat.impl.root_StartChatView_Day_3_de.png b/screenshots/de/features.startchat.impl.root_StartChatView_Day_3_de.png index 3320dc49a8..a9d8d8ef9b 100644 --- a/screenshots/de/features.startchat.impl.root_StartChatView_Day_3_de.png +++ b/screenshots/de/features.startchat.impl.root_StartChatView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec00175564a0a857d6bedd92eede06d191e9946224a76d8e4963c2d5748cf852 -size 55182 +oid sha256:4b9f41e1cd105f2c7b96ec1b3dc68008912442378075a93126d8b83e9c153dcb +size 55195 diff --git a/screenshots/de/features.startchat.impl.root_StartChatView_Day_4_de.png b/screenshots/de/features.startchat.impl.root_StartChatView_Day_4_de.png index f2029512b6..84083f34cf 100644 --- a/screenshots/de/features.startchat.impl.root_StartChatView_Day_4_de.png +++ b/screenshots/de/features.startchat.impl.root_StartChatView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57891ff2cc162505a6c4326d536720e49411f9eaf5a045b6d831b86c76fbef15 -size 45801 +oid sha256:edf88808f904a3cffd4c53e2d374ab3be9af91002b450de40d0e76b3f93d837c +size 45689 diff --git a/screenshots/de/features.startchat.impl.root_StartChatView_Day_5_de.png b/screenshots/de/features.startchat.impl.root_StartChatView_Day_5_de.png index 73a3eedd97..ae5b3d5707 100644 --- a/screenshots/de/features.startchat.impl.root_StartChatView_Day_5_de.png +++ b/screenshots/de/features.startchat.impl.root_StartChatView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aeaedf2423783724b72fcd49b036ee29ae702c13fc86960eea1d3f091995adbf -size 32105 +oid sha256:c6550e9568bf9360bdb33507122ac42220d4202cc691b01ed5f76ebe20beea12 +size 32104 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 c55dfeb43a..1b0c3f67bb 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:2ec98e8f379ff6f717e02e49a5ab28fa2f90061451ea2ee62034a8f01a1bc4f8 -size 25772 +oid sha256:5b193290d70ffa20cf5d4faac24d056a2d10e416d117ca29fbc007a5b1ad972e +size 25349 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 530494b9fb..4f743b5b98 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:d8b17dcc6496e77e90f10a31cbf83ea8a6d3bcf5ea7b2dfc735e845af78c8686 -size 23698 +oid sha256:fea501e631c2f1b84c1e8c61ad5267544f68e3c060a0fba575de8762fd7c8f8f +size 23281 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 ef5e672184..0d726b86c0 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:60ce2ab0c52103719c9ab7c62c18680e18c5d56b4d71203b93d221e95ea59dad -size 25354 +oid sha256:61a1d5027cce6975aacec3e0577b985180203ddeca950a8d9c4149138ece8a38 +size 25159 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 22db29b9fe..40f3a93772 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:d3e997bba5a039c42c17ae128d29c994c3fee668f152bbd267984d869d760f0f -size 26870 +oid sha256:b3812e73653f9c5f20b6a6ebb7710095567a7024e3a2ed7af25dd313994568df +size 26456 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_8_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_8_de.png index 7e06c715cd..f0650f7dd6 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileView_Day_8_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6ed7be40ce5e819c001d23564b1df9ad9aca051e2e6ea523cd44da5bab1b5af -size 37292 +oid sha256:c93e7f3b1a7e00fe70d42d9325245e04557fbd5be6752bda30ed0d845c8a7583 +size 37238 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 afc948e2be..f86f0e4348 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:223847851478472ab5ca7e918036e70a9629916ced775553c15483b32f558a5d -size 49804 +oid sha256:1a585cdbb479e4ab2aa53f9d0358a9a2068d2607094558f176a83bd2efcee48b +size 46009 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 index 022a8ca87c..88ce06753f 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_10_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0eb44dc561e133991106d30d8a8f67e92005a91483aa0f7f9e5029fd0ca2a3d -size 36319 +oid sha256:684222c49ca9fc06a9e63fcd46736b4c68b2f23f16e77228f78349f926514e6c +size 28094 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_de.png index 2512b6a0e1..f2f0404aad 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:446e8ab2ab97dac14c4852470276c42b81deca86258ad8971de14c88556d4101 -size 29923 +oid sha256:8efa1d431472915948e96b97a6a1b77997359c8b7a2a11ca038dc25d36d7cdc4 +size 29436 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 index df307d5881..5df6ba2298 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_12_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98727a2463f0a23be7bb0f4ee8daa76370de557ac265aae8c54c005218388fd2 -size 35381 +oid sha256:788f6e5a02171e885a321df9f523c9c0c87c86a183955936bfa1004027d1893a +size 32069 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 index df307d5881..5df6ba2298 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_13_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_13_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98727a2463f0a23be7bb0f4ee8daa76370de557ac265aae8c54c005218388fd2 -size 35381 +oid sha256:788f6e5a02171e885a321df9f523c9c0c87c86a183955936bfa1004027d1893a +size 32069 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 afc948e2be..f86f0e4348 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:223847851478472ab5ca7e918036e70a9629916ced775553c15483b32f558a5d -size 49804 +oid sha256:1a585cdbb479e4ab2aa53f9d0358a9a2068d2607094558f176a83bd2efcee48b +size 46009 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 c7acf9e1eb..d119111de8 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:e659117f1413c8204ab65e227948fa10fb031809816b58feb09e849b174475cb -size 41370 +oid sha256:ad9081355cb2897a5bf759d6f73920f5d9a1ae6d0ff7025df7c1200c4f25ecda +size 40869 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 e904d8e6a0..18c2ef6331 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:ef0c20276c21ed759f6325a5daa7eadb7c05ffd3c17c47697af9b5b4c05bdea7 -size 46388 +oid sha256:261f85d80baf9785fc8df2c83ba9978b1ba422ff129d264e8126e649fc7b95a5 +size 42458 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 bc56ffb6dc..e56aa1ca9f 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:696fa3ea9073b4a862fe21bd62ad29208b98f4179cadbf16a7a88170440cbf2d -size 36743 +oid sha256:d5615a84e510c1ee49321fba1bb2620042d32aa67d5197b8b88a642bf1c3edd9 +size 36232 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 b48478a927..095e794a2c 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:39f1ca571cc8cddfb12ea621735c5c3050f40560760bc91ddd0318275c1e6d93 -size 53934 +oid sha256:066c64055bc1034d2451d9d812b2b48692260062364424d2de5be2eca81d3922 +size 52848 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 b72bd254dc..98f53283c1 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:875af306179d0c780a12aef21256a991ed258b249fd5ad178067fc93cfed2773 -size 55644 +oid sha256:6ed6a28099f8902cf9174b3c9cdf4b7d154b623e7fe37b03c96813b708cb290b +size 53922 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 c83c9cc3ee..a6b85f2af9 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:2d77b9f2894a25092c74c3ad607f9d9a3695fb3d82b3187b01decddf97d8410a -size 45002 +oid sha256:355c47c2dbed84f808637b0093e7245667ad2dbffeabf84ea7f1244d377beca6 +size 43916 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 index 6bb622c47d..956be93533 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9763b719d7d2b338590582dc789dfabbf9f35171528e30ac67f1355678857a85 -size 46808 +oid sha256:b86b0973f12c87c98f5fa626d8761bf02372fe0820a5decf4947876adf1a2048 +size 45006 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_de.png index ab69508a97..be0253d957 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b6f190d51dfdce8096e223965ad4ea9bbd680d295836a6430980d8985471586 -size 35648 +oid sha256:1f97f11e866e1615cb826cdb6feeffa8ff106c5f41d3c9db9a262df5432ea05f +size 36929 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 index 8dad4963ef..49557625f5 100644 --- a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_de.png +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eda5b403a447a8392a2bbb8d3f479d85e81bf91b1ec071af6eda7947c6e90b1c -size 40461 +oid sha256:ea3c6696b24362b472019ca6fcee0012b3fa49eea419b8f8de93f34d8180ebc6 +size 34232 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 index 46fb1735ee..39db3ce4fa 100644 --- a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_de.png +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:012fe99c68b915382d12a4f3cdb2224e40b5d1a2806e4be816fbf8a31632cc1b -size 30787 +oid sha256:f983de6f7ed75b02a491fb7d4a9341b40a1f367d1e298a77ff8baf9b475a75c3 +size 30032 diff --git a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_de.png b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_de.png index 0e366f75dc..7d1347e719 100644 --- a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_de.png +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe4827772d4f44a82ab9dfae5d43a1a93885f4c443f21dc53e752e6ec582e946 -size 30037 +oid sha256:3ccf15ad76ec6abb1da4f73efdc98ae0fa74dadb2dc250c48567d5bd5c24154d +size 29551 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 index 39cbfc4757..3f36913215 100644 --- a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_de.png +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a17e2819004088741fdcea489d92b4197cfc97d9d52654be64523825f58807f1 -size 44022 +oid sha256:b9e4fd416b89d9c79d4c9413a45bcb9c455ed71d1e07184830bd7b91b61053a9 +size 41184 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 index d9ab695d25..cc63d4d5a6 100644 --- a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_de.png +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99317c30be5c5e1aab6db79aad273123a173d012e63b73ce4aa70efb82a900b2 -size 29868 +oid sha256:842f46fe337f143251bd8c2ec1e58e0823bb46c89cf82ef7491310dbb066d253 +size 25439 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 index fe3f860024..66e03de18d 100644 --- a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_de.png +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e179d57e7317b7e187220ac4a2498664115f599cb6aa2a330a3e718ff85959c -size 25981 +oid sha256:f83a6533053cead60f5bae4072546acefd5686a5f602b145bd9223626a8d8ee2 +size 21474 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 index b48478a927..095e794a2c 100644 --- a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_de.png +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39f1ca571cc8cddfb12ea621735c5c3050f40560760bc91ddd0318275c1e6d93 -size 53934 +oid sha256:066c64055bc1034d2451d9d812b2b48692260062364424d2de5be2eca81d3922 +size 52848 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 index b72bd254dc..98f53283c1 100644 --- a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_de.png +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:875af306179d0c780a12aef21256a991ed258b249fd5ad178067fc93cfed2773 -size 55644 +oid sha256:6ed6a28099f8902cf9174b3c9cdf4b7d154b623e7fe37b03c96813b708cb290b +size 53922 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 index c83c9cc3ee..a6b85f2af9 100644 --- a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_de.png +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d77b9f2894a25092c74c3ad607f9d9a3695fb3d82b3187b01decddf97d8410a -size 45002 +oid sha256:355c47c2dbed84f808637b0093e7245667ad2dbffeabf84ea7f1244d377beca6 +size 43916 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 index 5763dc29e5..425ec30050 100644 --- a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_de.png +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbe818ccd36be1b0a24782e10cb862c523bf4e971e131878c39a7d637c206070 -size 35381 +oid sha256:cf2239dea75e1981f0bd12c3dc0e1490b5ff9dadd29a12d605414d63bb4db6a6 +size 32068 diff --git a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_de.png b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_de.png index ab69508a97..be0253d957 100644 --- a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_de.png +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b6f190d51dfdce8096e223965ad4ea9bbd680d295836a6430980d8985471586 -size 35648 +oid sha256:1f97f11e866e1615cb826cdb6feeffa8ff106c5f41d3c9db9a262df5432ea05f +size 36929 diff --git a/screenshots/de/libraries.accountselect.impl_AccountSelectView_Day_0_de.png b/screenshots/de/libraries.accountselect.impl_AccountSelectView_Day_0_de.png new file mode 100644 index 0000000000..49108ddb0e --- /dev/null +++ b/screenshots/de/libraries.accountselect.impl_AccountSelectView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fb3f21b9b6460403f30709bc3464c8f584af34e7ac27083827a5c2c6121a2b1 +size 8243 diff --git a/screenshots/de/libraries.accountselect.impl_AccountSelectView_Day_1_de.png b/screenshots/de/libraries.accountselect.impl_AccountSelectView_Day_1_de.png new file mode 100644 index 0000000000..ce17201424 --- /dev/null +++ b/screenshots/de/libraries.accountselect.impl_AccountSelectView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf13a8c2d3b46a5f2e2b17b681b60ccc62dc886d137ec5464e35c80f4afe19fe +size 49022 diff --git a/screenshots/de/libraries.designsystem.components.async_AsyncActionView_Day_3_de.png b/screenshots/de/libraries.designsystem.components.async_AsyncActionView_Day_3_de.png index 40bb8c6698..ce4f4013b5 100644 --- a/screenshots/de/libraries.designsystem.components.async_AsyncActionView_Day_3_de.png +++ b/screenshots/de/libraries.designsystem.components.async_AsyncActionView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e7f87cbe57ac1f64685dff9df668d30311898b045596b3f2c928c3606ea129c +oid sha256:1b891e2217b77873c9f33f8377a0a1643a9e461e5ec4e67b817494194f4ec60c size 10478 diff --git a/screenshots/de/libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_de.png b/screenshots/de/libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_de.png index fa50461695..80fce53231 100644 --- a/screenshots/de/libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_de.png +++ b/screenshots/de/libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:674db98a0ff8dd9628b19583bf671b8eb7eefa13f3475525e14c8a61a5e05591 -size 14286 +oid sha256:490e87db0101dcf91fe86568322cacae54b657bd66de955e0dbbff2f5402396e +size 14285 diff --git a/screenshots/de/libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_de.png b/screenshots/de/libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_de.png index 1b305a5b9d..a43d60ba7c 100644 --- a/screenshots/de/libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_de.png +++ b/screenshots/de/libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e601b48a4db352012a7ce171f1ea6cf1e2241224dbec2944e9367e617348a86 -size 12864 +oid sha256:c1b59d7d1c8541af79bd99b45d5c4e7f0d17179c2f5a8f1c2feb694f92225c37 +size 12855 diff --git a/screenshots/de/libraries.designsystem.components.dialogs_ErrorDialog_Day_0_de.png b/screenshots/de/libraries.designsystem.components.dialogs_ErrorDialog_Day_0_de.png index bb113d1f29..61c16505c5 100644 --- a/screenshots/de/libraries.designsystem.components.dialogs_ErrorDialog_Day_0_de.png +++ b/screenshots/de/libraries.designsystem.components.dialogs_ErrorDialog_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bc1577f7e5d3125cd432cbc346803c81e9c4f24813241f90bc0542a0b2e5aa6 -size 9278 +oid sha256:16421610dab7afeaa01e67e1b48d5cff9e516d4bead702381fd00d3dfe75cd90 +size 9280 diff --git a/screenshots/de/libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_de.png b/screenshots/de/libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_de.png index 2958d50ac6..448999910f 100644 --- a/screenshots/de/libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_de.png +++ b/screenshots/de/libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9494488876263418a4c86505482910776062277385924f69e1dcb468d474991 -size 25944 +oid sha256:09ea44484d7dbedf8c37888360df25910f6cf03eb80924f5018500ade17c53c6 +size 25978 diff --git a/screenshots/de/libraries.designsystem.components.dialogs_RetryDialog_Day_0_de.png b/screenshots/de/libraries.designsystem.components.dialogs_RetryDialog_Day_0_de.png index ae23a0e725..78a751b5c4 100644 --- a/screenshots/de/libraries.designsystem.components.dialogs_RetryDialog_Day_0_de.png +++ b/screenshots/de/libraries.designsystem.components.dialogs_RetryDialog_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46767b660cc867c2a3a264c35ff8d1b91593ee0b888dcf4a40c67361c36b5a74 -size 15358 +oid sha256:dd9c051d47ffb1b91684e919796363b8d206fd2f2ab715a95ebb0e8e7e6000ca +size 15348 diff --git a/screenshots/de/libraries.designsystem.components_ProgressDialogWithContent_Day_0_de.png b/screenshots/de/libraries.designsystem.components_ProgressDialogWithContent_Day_0_de.png new file mode 100644 index 0000000000..72dc113ce5 --- /dev/null +++ b/screenshots/de/libraries.designsystem.components_ProgressDialogWithContent_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b110f2913fbdf487b46e92fec76ed372d7f89cab582d1ea9abb3fbaa2cd041f +size 12317 diff --git a/screenshots/de/libraries.matrix.ui.components_CheckableUnresolvedUserRow_de.png b/screenshots/de/libraries.matrix.ui.components_CheckableUnresolvedUserRow_de.png index d145b09412..b2f0e64495 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:343e168167bb1a5d49a58d5645b8aec6eab0d3e117e601535fbaa20b53b7c4ea -size 130978 +oid sha256:fde26e4404bb2a324cc098fc0c16dbfeabaf40fad7fc59c279c756e200b3a03b +size 127545 diff --git a/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_de.png index b039c5443a..64377be81c 100644 --- a/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_de.png +++ b/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:700e0f0019531df25af662e0f960e745344cc798667636d71981594cccc5715a -size 28744 +oid sha256:85f91b6c1665dba28fb428c0079a42b7ed3fc2f8e5f5b2f40df9c7f80bc47a3a +size 28681 diff --git a/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_de.png b/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_de.png index 077a8bb9b4..41c764998a 100644 --- a/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_de.png +++ b/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e6f7d8976e5fd8a35c023194c7d5addc0167f4f97e2146c0c2be118a0a9e2f6 -size 27030 +oid sha256:bcb0503b81b31809515e2c38824813e99ab7f9bcc9b79ee5822796656955890c +size 26977 diff --git a/screenshots/de/libraries.matrix.ui.components_OrganizationHeader_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_OrganizationHeader_Day_0_de.png new file mode 100644 index 0000000000..84a0e751df --- /dev/null +++ b/screenshots/de/libraries.matrix.ui.components_OrganizationHeader_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cfedb0a2c546985935c79948a8c9e766e1c26305ce5d503ca8f283a41a0d485 +size 41920 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_de.png new file mode 100644 index 0000000000..3265fe531a --- /dev/null +++ b/screenshots/de/libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bda85c0761b9ffe6b6cb21dffd5d5f2b0e7bbd3d9764210a4ee8bf042dba6efa +size 19839 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceHeaderView_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceHeaderView_Day_0_de.png new file mode 100644 index 0000000000..426d1477f8 --- /dev/null +++ b/screenshots/de/libraries.matrix.ui.components_SpaceHeaderView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1949da11938756f294347a4bc74d2dbef5dcb54364c55dcabcaea09bd6c13be8 +size 62269 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceInfoRow_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceInfoRow_Day_0_de.png new file mode 100644 index 0000000000..003a777ab1 --- /dev/null +++ b/screenshots/de/libraries.matrix.ui.components_SpaceInfoRow_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd08df4edb77aff2685a75d4e024e7eff82cba57515e9787074386aa70379bd3 +size 23041 diff --git a/screenshots/de/libraries.matrix.ui.components_UnresolvedUserRow_de.png b/screenshots/de/libraries.matrix.ui.components_UnresolvedUserRow_de.png index f095a364a7..7718515f1f 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:05ee0bc682ec4072e5d1f06731fd8bf7a8e601ba148283d8a5386586202037ff -size 40314 +oid sha256:c9e55c46d0f6633650ea00b79a97e79f140e2ecc27e4af979a4d7a8ce059d1f7 +size 73790 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 bce38076ef..873c014b18 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:eb9b147dc0a5ebd86aedf5cef09300872712616f19669fafd7969b158994728a -size 36486 +oid sha256:5a9b70ccd2182c71f91a41c4c4563719995c2b1735c576df4607685617052cc5 +size 35704 diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_de.png index fa03ecfd0b..7f5645467f 100644 --- a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6c9f9a367f05f667a91f8925d806ab70f6ade5db866a452a7c4ba6ba67ec51c -size 76029 +oid sha256:0c64b54d87184c26efafa8a527f4854251a08ebe099b80b8554baa5edd779dfe +size 75346 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 9983e1aa88..779fd077ba 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:025118ba6f44c729cfdf0fbc21ce767842d15dd936534724ba9316f045653f31 -size 82682 +oid sha256:6e640ef7595216d2c9365141799e3b5f4c636a2892fb0c21280ca7a9a87c0144 +size 82118 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 a84fdb5d7b..8d6b00f872 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:1f7cf384740483dea7acc6c99aa0aaad9c9bdfa4a3d60e4f2ac04d4c1698b959 -size 35939 +oid sha256:eb5df77727252a85aca9913b465026521178c9da77e27738df3bd0a99876676c +size 35317 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 4b6d14cdb7..39dedcbc2a 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:7c88b10eeb0b787624d12e653d4b7037cdc42b90a91c40ea23ab7b661af03625 -size 45999 +oid sha256:3d6bc0b609f19136a6bdf9752d399047aac63f26e88bea363d823b5273f1657d +size 45992 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 be893d4857..60c2b48abf 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:8e007e2bacf98c632d1bb9eda4fa04d3719538e4e9c301a645aaeeec477de546 -size 32161 +oid sha256:206efed2c6d28aab43b1588113335574e671a388e948d8de9a0ae63c0372c06a +size 30922 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 8244550c6e..e874e82876 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:a5e3427e220abb3b7a6c165cf9651d742c3b81b77f08f0f88575a8fa3aa25dd4 -size 29431 +oid sha256:284912820f3cc73c1067d45580f7bc07c743f2a21ab96842609f4b57ccc14fc6 +size 30861 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 f812bf4ac7..28037f69cb 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:e6e6930c02fbb1514ba608033dbc3b2ee5179463c83e5c1823a5330eec6467af -size 29817 +oid sha256:7d977bd436da02de2a76a0037e0704ba9878919b53dc502d963d6b6b8c9a9237 +size 29671 diff --git a/screenshots/de/libraries.permissions.api_PermissionsView_Day_3_de.png b/screenshots/de/libraries.permissions.api_PermissionsView_Day_3_de.png index 749747c45f..63e70d9fe8 100644 --- a/screenshots/de/libraries.permissions.api_PermissionsView_Day_3_de.png +++ b/screenshots/de/libraries.permissions.api_PermissionsView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75cdbf8a17df864faaec4ed2b228bb92c293e9d4e3385ddb11678cfff55976ed -size 22391 +oid sha256:f7390a48f7e11f4e907c89beedb1250c43312c63cc9ad54b2f9983e5f3fcb675 +size 24176 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 a793b7a3f6..b191e15948 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:2f28f4aa100b165a6e42c00954fe4db9d73ab22435030170350bea688bce4ec6 -size 32551 +oid sha256:726e9c2aa1399e8aca167d4dfc20684cbfa5ab0dae29465f5702ab1998887246 +size 32556 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 639a8ffd2b..6e92d8729f 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:d273538c58df7381a0fb1873a7a50e096e4c16c54682a5080f0bb1bde04f415e -size 30845 +oid sha256:1f5499db3bfde1f52df07f0378c506f45ba57bce0bcc5984627b9719d52efec8 +size 30902 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 06c0f25b69..b1e005c407 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:8b9c02c9680451c6e81910f6e64f418c11e8013d557d05928cbcd23934d57f60 -size 35108 +oid sha256:08cd625decbc86790b9d3ffa4dd250ddc45ab5e83758bc365b53dcc028a1ceb1 +size 35167 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 f312c6ca84..6a53fe6dc6 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:353823e3ae77c8ad1bbf0eb06e0d81f8244d67c7614fb049695c1d34f3967b7f -size 30229 +oid sha256:d739c07ec0320b2f9cb5229cbe1f851548b60affd674ada974c38ae4f1e2efc4 +size 30220 diff --git a/screenshots/de/libraries.troubleshoot.impl.history_PushHistoryView_Day_3_de.png b/screenshots/de/libraries.troubleshoot.impl.history_PushHistoryView_Day_3_de.png new file mode 100644 index 0000000000..f4c4da3dc5 --- /dev/null +++ b/screenshots/de/libraries.troubleshoot.impl.history_PushHistoryView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2fe5fd358af75454841c8648f64bdece8225e4327f5ec0d3e112d7b38da97fb1 +size 23951 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 2b3a69e096..6911e830d7 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:135a936cb7799292a925ad39bd0efdc7f22f30d582c92f1a7b1156717a999d8b -size 33690 +oid sha256:292f03d853dc79b3cfdaf60383feb0bfb28621f2ca222f861e0b8545e949626d +size 35608 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 e1219337db..4d4dc2092b 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:457f34602d2880887d69837acc8a262d659a77740b8e2af4e23968effa62982b -size 20383 +oid sha256:e9c5c2385a62311805385152b8986df99cdc7c0cc6558d774a01487c6591471b +size 20399 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 81582cab35..3b828cfa21 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:c452bf196838f1440c80099046f42854315ecd521adc6ad92eb84db7f7527f0d -size 20848 +oid sha256:4143aa80f5e04d2eb1a150bcf2ffd54eee46662e2d142bf4931b7ec2e877a913 +size 25803 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 0eb4797269..00b513f519 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:9d1f0416ac9143034e55d517bfa531c4ac5fdac1683ac8a71f43c9bdc25c75c6 -size 33736 +oid sha256:e4994247b57d45a9129a3ff1ff7ffc4d325153a76024f8ab853f68494fabbb2b +size 32984 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 fcb07dad50..f338319996 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:f35fafea2db7e6e158decbd84d17c2ac84ff69afc81aad2c63683206e07781ac +oid sha256:33e4eb4ceb6b959dc41aab717565b85fbd8837ed3318ffae42633d44ae9f0484 size 25633 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 f0fd4779ed..4c9c562a35 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:4d4af88d45b2f58a0118c1b588a1d056849e90c9fd8a1d2202047168434527d7 -size 41746 +oid sha256:f7e2ae21ea5a0ba84a46f04660ccbf6f6a7badbb6cd3e5ce3d00f2ab75ff33e9 +size 41747 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 81a994c223..3830d4d901 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:8934bc59ff99f34f7cadfa830350bbb9dc8327093ae4b52a022a3c03a8ede3af -size 27466 +oid sha256:697be9acc98d36ebdc113f3f8ca05268c5fdf1ee19c9268abff5c37e284b19e4 +size 27473 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 4113b0c70c..652a34b261 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:83d4e3a056cf69e140a39e0cf7f6cfe1c058358f6eb44e40197d9a1c0f5779ab -size 27236 +oid sha256:d992f1d26392afd09825656106a004f7b50b4a03896138730a15ccaf45296106 +size 26586 diff --git a/screenshots/de/services.apperror.impl_AppErrorView_Day_0_de.png b/screenshots/de/services.apperror.impl_AppErrorView_Day_0_de.png index 9116a57f72..f446c5ac38 100644 --- a/screenshots/de/services.apperror.impl_AppErrorView_Day_0_de.png +++ b/screenshots/de/services.apperror.impl_AppErrorView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26388ccf8c8d78a991bd46f39983536be27a1093d354436cfd3df466a414496b -size 19558 +oid sha256:43272bd940257366c0ac035787b1603fa5bd3903e67760a262c289a4e8ffb4e3 +size 19531 diff --git a/screenshots/html/data.js b/screenshots/html/data.js index 4870248cf5..ea8cc5e068 100644 --- a/screenshots/html/data.js +++ b/screenshots/html/data.js @@ -1,76 +1,79 @@ // 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",20318,], +["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",20360,], ["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",20318,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en",20318,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en",20318,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en",20318,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en",20318,], -["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20318,], -["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20318,], -["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20318,], -["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20318,], -["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20318,], -["features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en","features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en",20318,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en",20360,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en",20360,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en",20360,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en",20360,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en",20360,], +["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20360,], +["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20360,], +["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20360,], +["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20360,], +["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20360,], +["features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en","features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en",20360,], ["features.login.impl.accountprovider_AccountProviderView_Day_0_en","features.login.impl.accountprovider_AccountProviderView_Night_0_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_1_en","features.login.impl.accountprovider_AccountProviderView_Night_1_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_2_en","features.login.impl.accountprovider_AccountProviderView_Night_2_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_3_en","features.login.impl.accountprovider_AccountProviderView_Night_3_en",0,], +["libraries.accountselect.impl_AccountSelectView_Day_0_en","libraries.accountselect.impl_AccountSelectView_Night_0_en",20360,], +["libraries.accountselect.impl_AccountSelectView_Day_1_en","libraries.accountselect.impl_AccountSelectView_Night_1_en",20360,], ["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",20318,], -["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20318,], -["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20318,], +["features.messages.impl.actionlist_ActionListViewContent_Day_10_en","features.messages.impl.actionlist_ActionListViewContent_Night_10_en",20360,], +["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20360,], +["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20360,], ["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",20318,], -["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20318,], -["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20318,], -["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20318,], -["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20318,], -["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20318,], -["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20318,], -["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20318,], -["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20318,], -["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20318,], -["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en","",20318,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en","",20318,], -["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20318,], -["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20318,], -["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20318,], -["features.analytics.impl_AnalyticsOptInView_Day_1_en","features.analytics.impl_AnalyticsOptInView_Night_1_en",20318,], -["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20318,], -["features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en",20318,], -["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20318,], +["features.messages.impl.actionlist_ActionListViewContent_Day_2_en","features.messages.impl.actionlist_ActionListViewContent_Night_2_en",20360,], +["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20360,], +["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20360,], +["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20360,], +["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20360,], +["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20360,], +["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20360,], +["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20360,], +["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20360,], +["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20360,], +["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20360,], +["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en","",20360,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en","",20360,], +["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20360,], +["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20360,], +["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20360,], +["features.analytics.impl_AnalyticsOptInView_Day_1_en","features.analytics.impl_AnalyticsOptInView_Night_1_en",20360,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20360,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en",20360,], +["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20360,], ["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",20318,], +["services.apperror.impl_AppErrorView_Day_0_en","services.apperror.impl_AppErrorView_Night_0_en",20360,], ["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",20318,], +["libraries.designsystem.components.async_AsyncActionView_Day_1_en","libraries.designsystem.components.async_AsyncActionView_Night_1_en",20360,], ["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",20318,], +["libraries.designsystem.components.async_AsyncActionView_Day_3_en","libraries.designsystem.components.async_AsyncActionView_Night_3_en",20360,], ["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",20318,], +["libraries.designsystem.components.async_AsyncFailure_Day_0_en","libraries.designsystem.components.async_AsyncFailure_Night_0_en",20360,], ["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",20318,], +["features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en","features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en",20360,], ["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,], @@ -80,32 +83,62 @@ 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","",20318,], -["features.messages.impl.attachments.preview_AttachmentsView_1_en","",20318,], -["features.messages.impl.attachments.preview_AttachmentsView_2_en","",20318,], -["features.messages.impl.attachments.preview_AttachmentsView_3_en","",20318,], -["features.messages.impl.attachments.preview_AttachmentsView_4_en","",20318,], -["features.messages.impl.attachments.preview_AttachmentsView_5_en","",20318,], -["features.messages.impl.attachments.preview_AttachmentsView_6_en","",20318,], -["features.messages.impl.attachments.preview_AttachmentsView_7_en","",20318,], -["features.messages.impl.attachments.preview_AttachmentsView_8_en","",20318,], +["features.messages.impl.attachments.preview_AttachmentsView_0_en","",20360,], +["features.messages.impl.attachments.preview_AttachmentsView_1_en","",20360,], +["features.messages.impl.attachments.preview_AttachmentsView_2_en","",20360,], +["features.messages.impl.attachments.preview_AttachmentsView_3_en","",20360,], +["features.messages.impl.attachments.preview_AttachmentsView_4_en","",20360,], +["features.messages.impl.attachments.preview_AttachmentsView_5_en","",20360,], +["features.messages.impl.attachments.preview_AttachmentsView_6_en","",20360,], +["features.messages.impl.attachments.preview_AttachmentsView_7_en","",20360,], +["features.messages.impl.attachments.preview_AttachmentsView_8_en","",20360,], ["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",20318,], +["libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",20360,], ["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,], -["features.knockrequests.impl.banner_AvatarRowRtl_Day_3_en","features.knockrequests.impl.banner_AvatarRowRtl_Night_3_en",0,], -["features.knockrequests.impl.banner_AvatarRowRtl_Day_4_en","features.knockrequests.impl.banner_AvatarRowRtl_Night_4_en",0,], -["features.knockrequests.impl.banner_AvatarRow_Day_0_en","features.knockrequests.impl.banner_AvatarRow_Night_0_en",0,], -["features.knockrequests.impl.banner_AvatarRow_Day_1_en","features.knockrequests.impl.banner_AvatarRow_Night_1_en",0,], -["features.knockrequests.impl.banner_AvatarRow_Day_2_en","features.knockrequests.impl.banner_AvatarRow_Night_2_en",0,], -["features.knockrequests.impl.banner_AvatarRow_Day_3_en","features.knockrequests.impl.banner_AvatarRow_Night_3_en",0,], -["features.knockrequests.impl.banner_AvatarRow_Day_4_en","features.knockrequests.impl.banner_AvatarRow_Night_4_en",0,], +["libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_0_en","libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_0_en",0,], +["libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_1_en","libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_1_en",0,], +["libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_2_en","libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_2_en",0,], +["libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_3_en","libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_3_en",0,], +["libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_4_en","libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_4_en",0,], +["libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_0_en","libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_0_en",0,], +["libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_1_en","libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_1_en",0,], +["libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_2_en","libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_2_en",0,], +["libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_3_en","libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_3_en",0,], +["libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_4_en","libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_4_en",0,], +["libraries.designsystem.components.avatar_AvatarRowRtl_Day_0_en","libraries.designsystem.components.avatar_AvatarRowRtl_Night_0_en",0,], +["libraries.designsystem.components.avatar_AvatarRowRtl_Day_1_en","libraries.designsystem.components.avatar_AvatarRowRtl_Night_1_en",0,], +["libraries.designsystem.components.avatar_AvatarRowRtl_Day_2_en","libraries.designsystem.components.avatar_AvatarRowRtl_Night_2_en",0,], +["libraries.designsystem.components.avatar_AvatarRowRtl_Day_3_en","libraries.designsystem.components.avatar_AvatarRowRtl_Night_3_en",0,], +["libraries.designsystem.components.avatar_AvatarRowRtl_Day_4_en","libraries.designsystem.components.avatar_AvatarRowRtl_Night_4_en",0,], +["libraries.designsystem.components.avatar_AvatarRow_Day_0_en","libraries.designsystem.components.avatar_AvatarRow_Night_0_en",0,], +["libraries.designsystem.components.avatar_AvatarRow_Day_1_en","libraries.designsystem.components.avatar_AvatarRow_Night_1_en",0,], +["libraries.designsystem.components.avatar_AvatarRow_Day_2_en","libraries.designsystem.components.avatar_AvatarRow_Night_2_en",0,], +["libraries.designsystem.components.avatar_AvatarRow_Day_3_en","libraries.designsystem.components.avatar_AvatarRow_Night_3_en",0,], +["libraries.designsystem.components.avatar_AvatarRow_Day_4_en","libraries.designsystem.components.avatar_AvatarRow_Night_4_en",0,], ["libraries.designsystem.components.avatar_Avatar_Avatars_0_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_100_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_101_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_102_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_103_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_104_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_105_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_106_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_107_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_108_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_109_en","",0,], ["libraries.designsystem.components.avatar_Avatar_Avatars_10_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_110_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_111_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_112_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_113_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_114_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_115_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_116_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_117_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_118_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_119_en","",0,], ["libraries.designsystem.components.avatar_Avatar_Avatars_11_en","",0,], ["libraries.designsystem.components.avatar_Avatar_Avatars_12_en","",0,], ["libraries.designsystem.components.avatar_Avatar_Avatars_13_en","",0,], @@ -196,156 +229,166 @@ export const screenshots = [ ["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_93_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_94_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_95_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_96_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_97_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_98_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_99_en","",0,], ["libraries.designsystem.components.avatar_Avatar_Avatars_9_en","",0,], ["libraries.designsystem.components.button_BackButton_Buttons_en","",0,], ["libraries.designsystem.modifiers_BackgroundVerticalGradientDisabled_Day_0_en","libraries.designsystem.modifiers_BackgroundVerticalGradientDisabled_Night_0_en",0,], ["libraries.designsystem.modifiers_BackgroundVerticalGradientEnterprise_Day_0_en","libraries.designsystem.modifiers_BackgroundVerticalGradientEnterprise_Night_0_en",0,], ["libraries.designsystem.modifiers_BackgroundVerticalGradient_Day_0_en","libraries.designsystem.modifiers_BackgroundVerticalGradient_Night_0_en",0,], ["libraries.designsystem.components_Badge_Day_0_en","libraries.designsystem.components_Badge_Night_0_en",0,], -["features.home.impl.components_BatteryOptimizationBanner_Day_0_en","features.home.impl.components_BatteryOptimizationBanner_Night_0_en",20318,], +["features.home.impl.components_BatteryOptimizationBanner_Day_0_en","features.home.impl.components_BatteryOptimizationBanner_Night_0_en",20360,], ["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",20318,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20318,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20318,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20318,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20318,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20318,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20318,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_0_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_0_en",20360,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20360,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20360,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20360,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20360,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20360,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20360,], ["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",20318,], -["features.rageshake.impl.bugreport_BugReportView_Day_1_en","features.rageshake.impl.bugreport_BugReportView_Night_1_en",20318,], -["features.rageshake.impl.bugreport_BugReportView_Day_2_en","features.rageshake.impl.bugreport_BugReportView_Night_2_en",20318,], -["features.rageshake.impl.bugreport_BugReportView_Day_3_en","features.rageshake.impl.bugreport_BugReportView_Night_3_en",20318,], -["features.rageshake.impl.bugreport_BugReportView_Day_4_en","features.rageshake.impl.bugreport_BugReportView_Night_4_en",20318,], +["features.rageshake.impl.bugreport_BugReportView_Day_0_en","features.rageshake.impl.bugreport_BugReportView_Night_0_en",20360,], +["features.rageshake.impl.bugreport_BugReportView_Day_1_en","features.rageshake.impl.bugreport_BugReportView_Night_1_en",20360,], +["features.rageshake.impl.bugreport_BugReportView_Day_2_en","features.rageshake.impl.bugreport_BugReportView_Night_2_en",20360,], +["features.rageshake.impl.bugreport_BugReportView_Day_3_en","features.rageshake.impl.bugreport_BugReportView_Night_3_en",20360,], +["features.rageshake.impl.bugreport_BugReportView_Day_4_en","features.rageshake.impl.bugreport_BugReportView_Night_4_en",20360,], ["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",20318,], -["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20318,], +["features.messages.impl.timeline.components_CallMenuItem_Day_2_en","features.messages.impl.timeline.components_CallMenuItem_Night_2_en",20360,], +["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20360,], ["features.messages.impl.timeline.components_CallMenuItem_Day_4_en","features.messages.impl.timeline.components_CallMenuItem_Night_4_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",20318,], -["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20318,], -["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20318,], -["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20318,], -["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20318,], -["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en",20318,], -["features.changeroommemberroles.impl_ChangeRolesView_Day_0_en","features.changeroommemberroles.impl_ChangeRolesView_Night_0_en",20318,], -["features.changeroommemberroles.impl_ChangeRolesView_Day_10_en","features.changeroommemberroles.impl_ChangeRolesView_Night_10_en",20318,], -["features.changeroommemberroles.impl_ChangeRolesView_Day_11_en","features.changeroommemberroles.impl_ChangeRolesView_Night_11_en",20318,], -["features.changeroommemberroles.impl_ChangeRolesView_Day_12_en","features.changeroommemberroles.impl_ChangeRolesView_Night_12_en",20318,], -["features.changeroommemberroles.impl_ChangeRolesView_Day_1_en","features.changeroommemberroles.impl_ChangeRolesView_Night_1_en",20318,], -["features.changeroommemberroles.impl_ChangeRolesView_Day_2_en","features.changeroommemberroles.impl_ChangeRolesView_Night_2_en",20318,], -["features.changeroommemberroles.impl_ChangeRolesView_Day_3_en","features.changeroommemberroles.impl_ChangeRolesView_Night_3_en",20318,], -["features.changeroommemberroles.impl_ChangeRolesView_Day_4_en","features.changeroommemberroles.impl_ChangeRolesView_Night_4_en",20318,], -["features.changeroommemberroles.impl_ChangeRolesView_Day_5_en","features.changeroommemberroles.impl_ChangeRolesView_Night_5_en",20318,], -["features.changeroommemberroles.impl_ChangeRolesView_Day_6_en","features.changeroommemberroles.impl_ChangeRolesView_Night_6_en",20318,], -["features.changeroommemberroles.impl_ChangeRolesView_Day_7_en","features.changeroommemberroles.impl_ChangeRolesView_Night_7_en",20318,], -["features.changeroommemberroles.impl_ChangeRolesView_Day_8_en","features.changeroommemberroles.impl_ChangeRolesView_Night_8_en",20318,], -["features.changeroommemberroles.impl_ChangeRolesView_Day_9_en","features.changeroommemberroles.impl_ChangeRolesView_Night_9_en",20318,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_0_en",20318,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_1_en",20318,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_2_en",20318,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_3_en",20318,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_4_en",20318,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_5_en",20318,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_6_en",20318,], +["features.call.impl.ui_CallScreenView_Day_1_en","features.call.impl.ui_CallScreenView_Night_1_en",20360,], +["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20360,], +["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20360,], +["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20360,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20360,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en",20360,], +["features.changeroommemberroles.impl_ChangeRolesView_Day_0_en","features.changeroommemberroles.impl_ChangeRolesView_Night_0_en",20360,], +["features.changeroommemberroles.impl_ChangeRolesView_Day_10_en","features.changeroommemberroles.impl_ChangeRolesView_Night_10_en",20360,], +["features.changeroommemberroles.impl_ChangeRolesView_Day_11_en","features.changeroommemberroles.impl_ChangeRolesView_Night_11_en",20360,], +["features.changeroommemberroles.impl_ChangeRolesView_Day_12_en","features.changeroommemberroles.impl_ChangeRolesView_Night_12_en",20360,], +["features.changeroommemberroles.impl_ChangeRolesView_Day_1_en","features.changeroommemberroles.impl_ChangeRolesView_Night_1_en",20360,], +["features.changeroommemberroles.impl_ChangeRolesView_Day_2_en","features.changeroommemberroles.impl_ChangeRolesView_Night_2_en",20360,], +["features.changeroommemberroles.impl_ChangeRolesView_Day_3_en","features.changeroommemberroles.impl_ChangeRolesView_Night_3_en",20360,], +["features.changeroommemberroles.impl_ChangeRolesView_Day_4_en","features.changeroommemberroles.impl_ChangeRolesView_Night_4_en",20360,], +["features.changeroommemberroles.impl_ChangeRolesView_Day_5_en","features.changeroommemberroles.impl_ChangeRolesView_Night_5_en",0,], +["features.changeroommemberroles.impl_ChangeRolesView_Day_6_en","features.changeroommemberroles.impl_ChangeRolesView_Night_6_en",20360,], +["features.changeroommemberroles.impl_ChangeRolesView_Day_7_en","features.changeroommemberroles.impl_ChangeRolesView_Night_7_en",20360,], +["features.changeroommemberroles.impl_ChangeRolesView_Day_8_en","features.changeroommemberroles.impl_ChangeRolesView_Night_8_en",20360,], +["features.changeroommemberroles.impl_ChangeRolesView_Day_9_en","features.changeroommemberroles.impl_ChangeRolesView_Night_9_en",20360,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_0_en",20360,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_1_en",20360,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_2_en",20360,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_3_en",20360,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_4_en",20360,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_5_en",20360,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_6_en",20360,], ["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",20318,], -["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20318,], -["features.login.impl.changeserver_ChangeServerView_Day_3_en","features.login.impl.changeserver_ChangeServerView_Night_3_en",20318,], -["features.login.impl.changeserver_ChangeServerView_Day_4_en","features.login.impl.changeserver_ChangeServerView_Night_4_en",20318,], +["features.login.impl.changeserver_ChangeServerView_Day_1_en","features.login.impl.changeserver_ChangeServerView_Night_1_en",20360,], +["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20360,], +["features.login.impl.changeserver_ChangeServerView_Day_3_en","features.login.impl.changeserver_ChangeServerView_Night_3_en",20360,], +["features.login.impl.changeserver_ChangeServerView_Day_4_en","features.login.impl.changeserver_ChangeServerView_Night_4_en",20360,], ["libraries.matrix.ui.components_CheckableResolvedUserRow_en","",0,], -["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20318,], +["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20360,], ["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",20318,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en",20318,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en",20318,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en",20318,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en",20318,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en",20318,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en",20318,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en",20360,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en",20360,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en",20360,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en",20360,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en",20360,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en",20360,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en",20360,], ["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",20318,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20318,], -["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20318,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en",20360,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20360,], +["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20360,], ["libraries.textcomposer_ComposerModeView_Day_1_en","libraries.textcomposer_ComposerModeView_Night_1_en",0,], ["libraries.textcomposer_ComposerModeView_Day_2_en","libraries.textcomposer_ComposerModeView_Night_2_en",0,], ["libraries.textcomposer_ComposerModeView_Day_3_en","libraries.textcomposer_ComposerModeView_Night_3_en",0,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20318,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20318,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20318,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20318,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20318,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20318,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20318,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20318,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20318,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20318,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20318,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20318,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20318,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20318,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20318,], -["features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20318,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20360,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20360,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20360,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20360,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20360,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20360,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20360,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20360,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20360,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20360,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20360,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20360,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20360,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20360,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20360,], +["features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20360,], ["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",20318,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20318,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20318,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20318,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20318,], -["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en",20318,], -["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en",20318,], -["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20318,], -["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20318,], -["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20318,], -["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20318,], -["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20318,], -["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20318,], -["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20318,], -["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20318,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20318,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20318,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20318,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20318,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20318,], +["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",20360,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20360,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20360,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20360,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20360,], +["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en",20360,], +["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en",20360,], +["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20360,], +["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20360,], +["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20360,], +["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20360,], +["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20360,], +["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20360,], +["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20360,], +["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20360,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20360,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20360,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20360,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20360,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20360,], ["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","",20318,], -["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20318,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en",20318,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en",20318,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en",20318,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en",20318,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en",20318,], +["libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en","",20360,], +["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20360,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en",20360,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en",20360,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en",20360,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en",20360,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en",20360,], ["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",20318,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20318,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20318,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_1_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_1_en",20360,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20360,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20360,], ["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",20318,], -["features.home.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_en","features.home.impl.components_DefaultRoomListTopBarWithIndicator_Night_0_en",20318,], -["features.home.impl.components_DefaultRoomListTopBar_Day_0_en","features.home.impl.components_DefaultRoomListTopBar_Night_0_en",20318,], +["features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en","features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en",20360,], +["features.home.impl.components_DefaultRoomListTopBarMultiAccount_Day_0_en","features.home.impl.components_DefaultRoomListTopBarMultiAccount_Night_0_en",20360,], +["features.home.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_en","features.home.impl.components_DefaultRoomListTopBarWithIndicator_Night_0_en",20360,], +["features.home.impl.components_DefaultRoomListTopBar_Day_0_en","features.home.impl.components_DefaultRoomListTopBar_Night_0_en",20360,], ["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",20318,], -["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20318,], -["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20318,], -["features.licenses.impl.list_DependencyLicensesListView_Day_3_en","features.licenses.impl.list_DependencyLicensesListView_Night_3_en",20318,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20318,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20318,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20318,], +["features.licenses.impl.list_DependencyLicensesListView_Day_0_en","features.licenses.impl.list_DependencyLicensesListView_Night_0_en",20360,], +["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20360,], +["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20360,], +["features.licenses.impl.list_DependencyLicensesListView_Day_3_en","features.licenses.impl.list_DependencyLicensesListView_Night_3_en",20360,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20360,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20360,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20360,], ["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.designsystem.theme.components_DialogWithVeryLongTitleAndIcon_Dialog_with_a_very_long_title_and_icon_Dialogs_en","",0,], +["libraries.designsystem.theme.components_DialogWithVeryLongTitle_Dialog_with_a_very_long_title_Dialogs_en","",0,], ["features.messages.impl.messagecomposer_DisabledComposerView_Day_0_en","features.messages.impl.messagecomposer_DisabledComposerView_Night_0_en",0,], ["libraries.designsystem.components.avatar_DmAvatarsRtl_Avatars_en","",0,], ["libraries.designsystem.components.avatar_DmAvatars_Avatars_en","",0,], @@ -353,30 +396,35 @@ export const screenshots = [ ["libraries.designsystem.text_DpScale_1_0f__en","",0,], ["libraries.designsystem.text_DpScale_1_5f__en","",0,], ["libraries.designsystem.theme.components_DropdownMenuItem_Menus_en","",0,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en",20318,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20318,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20318,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20318,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20318,], -["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_0_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_0_en",20318,], -["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_1_en",20318,], -["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_2_en",20318,], -["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_3_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_3_en",20318,], -["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_4_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_4_en",20318,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20318,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en",20318,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en",20360,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20360,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20360,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20360,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20360,], +["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_0_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_0_en",20360,], +["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_1_en",20360,], +["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_2_en",20360,], +["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_3_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_3_en",20360,], +["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_4_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_4_en",20360,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20360,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en",20360,], ["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,], +["libraries.matrix.ui.components_EditableOrgAvatarRtl_Day_0_en","libraries.matrix.ui.components_EditableOrgAvatarRtl_Night_0_en",0,], +["libraries.matrix.ui.components_EditableOrgAvatar_Day_0_en","libraries.matrix.ui.components_EditableOrgAvatar_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_ElementLogoAtomLargeNoBlurShadow_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomLargeNoBlurShadow_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_ElementLogoAtomLarge_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomLarge_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_ElementLogoAtomMediumNoBlurShadow_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomMediumNoBlurShadow_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_ElementLogoAtomMedium_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomMedium_Night_0_en",0,], ["features.messages.impl.timeline.components.customreaction_EmojiItem_Day_0_en","features.messages.impl.timeline.components.customreaction_EmojiItem_Night_0_en",0,], -["features.messages.impl.timeline.components.customreaction_EmojiPicker_Day_0_en","features.messages.impl.timeline.components.customreaction_EmojiPicker_Night_0_en",0,], -["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20318,], -["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20318,], -["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20318,], +["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en",20360,], +["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en",20360,], +["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_2_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_2_en",0,], +["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_3_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_3_en",0,], +["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20360,], +["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20360,], +["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20360,], ["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,], @@ -395,38 +443,41 @@ 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",20318,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20318,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20318,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_1_en",20360,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20360,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20360,], ["features.messages.impl.timeline.components_FocusedEventEnterprise_Day_0_en","features.messages.impl.timeline.components_FocusedEventEnterprise_Night_0_en",0,], ["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",20318,], -["features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20318,], +["features.messages.impl.forward_ForwardMessagesView_Day_3_en","features.messages.impl.forward_ForwardMessagesView_Night_3_en",20360,], +["features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20360,], ["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",20318,], -["features.home.impl_HomeView_Day_10_en","features.home.impl_HomeView_Night_10_en",20318,], +["features.home.impl.spaces_HomeSpacesView_Day_0_en","features.home.impl.spaces_HomeSpacesView_Night_0_en",20360,], +["features.home.impl.spaces_HomeSpacesView_Day_1_en","features.home.impl.spaces_HomeSpacesView_Night_1_en",20360,], +["features.home.impl_HomeViewA11y_en","",0,], +["features.home.impl_HomeView_Day_0_en","features.home.impl_HomeView_Night_0_en",20360,], +["features.home.impl_HomeView_Day_10_en","features.home.impl_HomeView_Night_10_en",20360,], ["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",20318,], -["features.home.impl_HomeView_Day_14_en","features.home.impl_HomeView_Night_14_en",20318,], -["features.home.impl_HomeView_Day_15_en","features.home.impl_HomeView_Night_15_en",20318,], -["features.home.impl_HomeView_Day_1_en","features.home.impl_HomeView_Night_1_en",20318,], -["features.home.impl_HomeView_Day_2_en","features.home.impl_HomeView_Night_2_en",20318,], -["features.home.impl_HomeView_Day_3_en","features.home.impl_HomeView_Night_3_en",20318,], -["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",20318,], -["features.home.impl_HomeView_Day_6_en","features.home.impl_HomeView_Night_6_en",20318,], -["features.home.impl_HomeView_Day_7_en","features.home.impl_HomeView_Night_7_en",20318,], -["features.home.impl_HomeView_Day_8_en","features.home.impl_HomeView_Night_8_en",20318,], -["features.home.impl_HomeView_Day_9_en","features.home.impl_HomeView_Night_9_en",20318,], +["features.home.impl_HomeView_Day_13_en","features.home.impl_HomeView_Night_13_en",20360,], +["features.home.impl_HomeView_Day_14_en","features.home.impl_HomeView_Night_14_en",20360,], +["features.home.impl_HomeView_Day_15_en","features.home.impl_HomeView_Night_15_en",20360,], +["features.home.impl_HomeView_Day_1_en","features.home.impl_HomeView_Night_1_en",20360,], +["features.home.impl_HomeView_Day_2_en","features.home.impl_HomeView_Night_2_en",20360,], +["features.home.impl_HomeView_Day_3_en","features.home.impl_HomeView_Night_3_en",20360,], +["features.home.impl_HomeView_Day_4_en","features.home.impl_HomeView_Night_4_en",20360,], +["features.home.impl_HomeView_Day_5_en","features.home.impl_HomeView_Night_5_en",20360,], +["features.home.impl_HomeView_Day_6_en","features.home.impl_HomeView_Night_6_en",20360,], +["features.home.impl_HomeView_Day_7_en","features.home.impl_HomeView_Night_7_en",20360,], +["features.home.impl_HomeView_Day_8_en","features.home.impl_HomeView_Night_8_en",20360,], +["features.home.impl_HomeView_Day_9_en","features.home.impl_HomeView_Night_9_en",20360,], ["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,], @@ -435,8 +486,8 @@ export const screenshots = [ ["libraries.designsystem.atomic.molecules_IconTitlePlaceholdersRowMolecule_Day_0_en","libraries.designsystem.atomic.molecules_IconTitlePlaceholdersRowMolecule_Night_0_en",0,], ["libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Day_0_en","libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Night_0_en",0,], ["libraries.designsystem.theme.components_IconToggleButton_Toggles_en","",0,], -["appicon.enterprise_Icon_en","",0,], ["appicon.element_Icon_en","",0,], +["appicon.enterprise_Icon_en","",0,], ["libraries.designsystem.icons_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,], @@ -445,8 +496,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",20318,], -["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20318,], +["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en",20360,], +["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20360,], ["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,], @@ -454,95 +505,106 @@ 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",20318,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_4_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_4_en",20360,], ["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",20318,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_8_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_8_en",20360,], ["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",20318,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20318,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en",20318,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en",20318,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en",20318,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en",20318,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20318,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20318,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20318,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20318,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20318,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20318,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20318,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en",20318,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en",20318,], +["features.call.impl.ui_IncomingCallScreen_Day_0_en","features.call.impl.ui_IncomingCallScreen_Night_0_en",20360,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20360,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en",20360,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en",20360,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en",20360,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en",20360,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20360,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20360,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20360,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20360,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20360,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20360,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20360,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en",20360,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en",20360,], ["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,], -["features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en","features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en",20318,], -["features.invitepeople.impl_InvitePeopleView_Day_0_en","features.invitepeople.impl_InvitePeopleView_Night_0_en",20318,], -["features.invitepeople.impl_InvitePeopleView_Day_1_en","features.invitepeople.impl_InvitePeopleView_Night_1_en",20318,], +["libraries.matrix.ui.media_InitialsAvatarBitmapGenerator_Day_0_en","libraries.matrix.ui.media_InitialsAvatarBitmapGenerator_Night_0_en",0,], +["features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en","features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en",20360,], +["features.invitepeople.impl_InvitePeopleView_Day_0_en","features.invitepeople.impl_InvitePeopleView_Night_0_en",20360,], +["features.invitepeople.impl_InvitePeopleView_Day_1_en","features.invitepeople.impl_InvitePeopleView_Night_1_en",20360,], ["features.invitepeople.impl_InvitePeopleView_Day_2_en","features.invitepeople.impl_InvitePeopleView_Night_2_en",0,], ["features.invitepeople.impl_InvitePeopleView_Day_3_en","features.invitepeople.impl_InvitePeopleView_Night_3_en",0,], -["features.invitepeople.impl_InvitePeopleView_Day_4_en","features.invitepeople.impl_InvitePeopleView_Night_4_en",20318,], -["features.invitepeople.impl_InvitePeopleView_Day_5_en","features.invitepeople.impl_InvitePeopleView_Night_5_en",20318,], -["features.invitepeople.impl_InvitePeopleView_Day_6_en","features.invitepeople.impl_InvitePeopleView_Night_6_en",20318,], -["features.invitepeople.impl_InvitePeopleView_Day_7_en","features.invitepeople.impl_InvitePeopleView_Night_7_en",20318,], +["features.invitepeople.impl_InvitePeopleView_Day_4_en","features.invitepeople.impl_InvitePeopleView_Night_4_en",20360,], +["features.invitepeople.impl_InvitePeopleView_Day_5_en","features.invitepeople.impl_InvitePeopleView_Night_5_en",20360,], +["features.invitepeople.impl_InvitePeopleView_Day_6_en","features.invitepeople.impl_InvitePeopleView_Night_6_en",20360,], +["features.invitepeople.impl_InvitePeopleView_Day_7_en","features.invitepeople.impl_InvitePeopleView_Night_7_en",20360,], ["features.invitepeople.impl_InvitePeopleView_Day_8_en","features.invitepeople.impl_InvitePeopleView_Night_8_en",0,], -["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20318,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en",20318,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en",20318,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en",20318,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en",20318,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en",20318,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en",20318,], +["features.invitepeople.impl_InvitePeopleView_Day_9_en","features.invitepeople.impl_InvitePeopleView_Night_9_en",20360,], +["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20360,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en",20360,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en",20360,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en",20360,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en",20360,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en",20360,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en",20360,], ["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",20318,], -["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20318,], -["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20318,], -["features.joinroom.impl_JoinRoomView_Day_13_en","features.joinroom.impl_JoinRoomView_Night_13_en",20318,], -["features.joinroom.impl_JoinRoomView_Day_14_en","features.joinroom.impl_JoinRoomView_Night_14_en",20318,], -["features.joinroom.impl_JoinRoomView_Day_15_en","features.joinroom.impl_JoinRoomView_Night_15_en",20318,], -["features.joinroom.impl_JoinRoomView_Day_16_en","features.joinroom.impl_JoinRoomView_Night_16_en",20318,], -["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20318,], -["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20318,], -["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20318,], -["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20318,], -["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20318,], -["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20318,], -["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20318,], -["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20318,], -["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",20318,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20318,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20318,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20318,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20318,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20318,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20318,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20318,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20318,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_10_en","features.knockrequests.impl.list_KnockRequestsListView_Night_10_en",20318,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20318,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20318,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20318,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20318,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20318,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20318,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20318,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20318,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20318,], +["features.joinroom.impl_JoinRoomView_Day_10_en","features.joinroom.impl_JoinRoomView_Night_10_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_13_en","features.joinroom.impl_JoinRoomView_Night_13_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_14_en","features.joinroom.impl_JoinRoomView_Night_14_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_15_en","features.joinroom.impl_JoinRoomView_Night_15_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_16_en","features.joinroom.impl_JoinRoomView_Night_16_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20360,], +["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",20360,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20360,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20360,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20360,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20360,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20360,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20360,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20360,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20360,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_10_en","features.knockrequests.impl.list_KnockRequestsListView_Night_10_en",20360,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20360,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20360,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20360,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20360,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20360,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20360,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20360,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20360,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20360,], ["libraries.designsystem.components_LabelledCheckbox_Toggles_en","",0,], ["features.leaveroom.impl_LeaveRoomView_Day_0_en","features.leaveroom.impl_LeaveRoomView_Night_0_en",0,], -["features.leaveroom.impl_LeaveRoomView_Day_1_en","features.leaveroom.impl_LeaveRoomView_Night_1_en",20318,], -["features.leaveroom.impl_LeaveRoomView_Day_2_en","features.leaveroom.impl_LeaveRoomView_Night_2_en",20318,], -["features.leaveroom.impl_LeaveRoomView_Day_3_en","features.leaveroom.impl_LeaveRoomView_Night_3_en",20318,], -["features.leaveroom.impl_LeaveRoomView_Day_4_en","features.leaveroom.impl_LeaveRoomView_Night_4_en",20318,], -["features.leaveroom.impl_LeaveRoomView_Day_5_en","features.leaveroom.impl_LeaveRoomView_Night_5_en",20318,], -["features.leaveroom.impl_LeaveRoomView_Day_6_en","features.leaveroom.impl_LeaveRoomView_Night_6_en",20318,], -["features.leaveroom.impl_LeaveRoomView_Day_7_en","features.leaveroom.impl_LeaveRoomView_Night_7_en",20318,], +["features.leaveroom.impl_LeaveRoomView_Day_1_en","features.leaveroom.impl_LeaveRoomView_Night_1_en",20360,], +["features.leaveroom.impl_LeaveRoomView_Day_2_en","features.leaveroom.impl_LeaveRoomView_Night_2_en",20360,], +["features.leaveroom.impl_LeaveRoomView_Day_3_en","features.leaveroom.impl_LeaveRoomView_Night_3_en",20360,], +["features.leaveroom.impl_LeaveRoomView_Day_4_en","features.leaveroom.impl_LeaveRoomView_Night_4_en",20360,], +["features.leaveroom.impl_LeaveRoomView_Day_5_en","features.leaveroom.impl_LeaveRoomView_Night_5_en",20360,], +["features.leaveroom.impl_LeaveRoomView_Day_6_en","features.leaveroom.impl_LeaveRoomView_Night_6_en",20360,], +["features.leaveroom.impl_LeaveRoomView_Day_7_en","features.leaveroom.impl_LeaveRoomView_Night_7_en",20360,], +["features.space.impl.leave_LeaveSpaceView_Day_0_en","features.space.impl.leave_LeaveSpaceView_Night_0_en",20360,], +["features.space.impl.leave_LeaveSpaceView_Day_1_en","features.space.impl.leave_LeaveSpaceView_Night_1_en",20360,], +["features.space.impl.leave_LeaveSpaceView_Day_2_en","features.space.impl.leave_LeaveSpaceView_Night_2_en",20360,], +["features.space.impl.leave_LeaveSpaceView_Day_3_en","features.space.impl.leave_LeaveSpaceView_Night_3_en",20360,], +["features.space.impl.leave_LeaveSpaceView_Day_4_en","features.space.impl.leave_LeaveSpaceView_Night_4_en",20360,], +["features.space.impl.leave_LeaveSpaceView_Day_5_en","features.space.impl.leave_LeaveSpaceView_Night_5_en",20360,], +["features.space.impl.leave_LeaveSpaceView_Day_6_en","features.space.impl.leave_LeaveSpaceView_Night_6_en",20360,], +["features.space.impl.leave_LeaveSpaceView_Day_7_en","features.space.impl.leave_LeaveSpaceView_Night_7_en",20360,], +["features.space.impl.leave_LeaveSpaceView_Day_8_en","features.space.impl.leave_LeaveSpaceView_Night_8_en",20360,], ["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",20318,], +["features.messages.impl.link_LinkView_Day_1_en","features.messages.impl.link_LinkView_Night_1_en",20360,], ["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,], @@ -597,36 +659,37 @@ 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",20318,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20318,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20318,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20318,], +["appnav.room.joined_LoadingRoomNodeView_Day_1_en","appnav.room.joined_LoadingRoomNodeView_Night_1_en",20360,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20360,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20360,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20360,], ["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",20318,], -["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20318,], -["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20318,], -["features.login.impl.login_LoginModeView_Day_0_en","features.login.impl.login_LoginModeView_Night_0_en",20318,], -["features.login.impl.login_LoginModeView_Day_1_en","features.login.impl.login_LoginModeView_Night_1_en",20318,], -["features.login.impl.login_LoginModeView_Day_2_en","features.login.impl.login_LoginModeView_Night_2_en",20318,], -["features.login.impl.login_LoginModeView_Day_3_en","features.login.impl.login_LoginModeView_Night_3_en",20318,], -["features.login.impl.login_LoginModeView_Day_4_en","features.login.impl.login_LoginModeView_Night_4_en",20318,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20318,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20318,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20318,], -["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20318,], -["features.logout.impl_LogoutView_Day_10_en","features.logout.impl_LogoutView_Night_10_en",20318,], -["features.logout.impl_LogoutView_Day_11_en","features.logout.impl_LogoutView_Night_11_en",20318,], -["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20318,], -["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20318,], -["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20318,], -["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20318,], -["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20318,], -["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20318,], -["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20318,], -["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20318,], -["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20318,], +["appnav.loggedin_LoggedInView_Day_1_en","appnav.loggedin_LoggedInView_Night_1_en",20360,], +["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20360,], +["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20360,], +["features.login.impl.login_LoginModeView_Day_0_en","features.login.impl.login_LoginModeView_Night_0_en",20360,], +["features.login.impl.login_LoginModeView_Day_1_en","features.login.impl.login_LoginModeView_Night_1_en",20360,], +["features.login.impl.login_LoginModeView_Day_2_en","features.login.impl.login_LoginModeView_Night_2_en",20360,], +["features.login.impl.login_LoginModeView_Day_3_en","features.login.impl.login_LoginModeView_Night_3_en",20360,], +["features.login.impl.login_LoginModeView_Day_4_en","features.login.impl.login_LoginModeView_Night_4_en",20360,], +["features.login.impl.login_LoginModeView_Day_5_en","features.login.impl.login_LoginModeView_Night_5_en",20360,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20360,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20360,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20360,], +["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20360,], +["features.logout.impl_LogoutView_Day_10_en","features.logout.impl_LogoutView_Night_10_en",20360,], +["features.logout.impl_LogoutView_Day_11_en","features.logout.impl_LogoutView_Night_11_en",20360,], +["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20360,], +["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20360,], +["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20360,], +["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20360,], +["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20360,], +["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20360,], +["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20360,], +["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20360,], +["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20360,], ["libraries.designsystem.components.button_MainActionButton_Buttons_en","",0,], -["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20318,], +["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20360,], ["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,], @@ -639,22 +702,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",20318,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20318,], +["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en",20360,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20360,], ["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",20318,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20318,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_11_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_11_en",20318,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_12_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_12_en",20318,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20318,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20318,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20318,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20318,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20318,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20318,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20318,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20318,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20318,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en",20360,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20360,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_11_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_11_en",20360,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_12_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_12_en",20360,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20360,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20360,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20360,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20360,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20360,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20360,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20360,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20360,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20360,], ["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,], @@ -662,14 +725,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","",20318,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20318,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_11_en","",20360,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20360,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_13_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_14_en","",20318,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_14_en","",20360,], ["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","",20318,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_2_en","",20360,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_3_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_4_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_5_en","",0,], @@ -678,11 +741,12 @@ export const screenshots = [ ["libraries.mediaviewer.impl.viewer_MediaViewerView_8_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_9_en","",0,], ["libraries.designsystem.theme.components_MediumTopAppBar_App_Bars_en","",0,], +["libraries.designsystem.atomic.molecules_MembersCountMolecule_Day_0_en","libraries.designsystem.atomic.molecules_MembersCountMolecule_Night_0_en",0,], ["libraries.textcomposer.mentions_MentionSpanThemeInTimeline_Day_0_en","libraries.textcomposer.mentions_MentionSpanThemeInTimeline_Night_0_en",0,], ["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",20318,], +["features.messages.impl.messagecomposer_MessageComposerView_Day_0_en","features.messages.impl.messagecomposer_MessageComposerView_Night_0_en",20360,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_0_en","features.messages.impl.timeline.components_MessageEventBubble_Night_0_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_1_en","features.messages.impl.timeline.components_MessageEventBubble_Night_1_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_2_en","features.messages.impl.timeline.components_MessageEventBubble_Night_2_en",0,], @@ -691,7 +755,7 @@ export const screenshots = [ ["features.messages.impl.timeline.components_MessageEventBubble_Day_5_en","features.messages.impl.timeline.components_MessageEventBubble_Night_5_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_6_en","features.messages.impl.timeline.components_MessageEventBubble_Night_6_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_7_en","features.messages.impl.timeline.components_MessageEventBubble_Night_7_en",0,], -["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20318,], +["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20360,], ["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,], @@ -699,136 +763,137 @@ 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",20318,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20318,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20318,], -["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20318,], -["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20318,], -["features.messages.impl_MessagesView_Day_11_en","features.messages.impl_MessagesView_Night_11_en",20318,], -["features.messages.impl_MessagesView_Day_12_en","features.messages.impl_MessagesView_Night_12_en",20318,], -["features.messages.impl_MessagesView_Day_13_en","features.messages.impl_MessagesView_Night_13_en",20318,], -["features.messages.impl_MessagesView_Day_14_en","features.messages.impl_MessagesView_Night_14_en",20318,], -["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20318,], -["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20318,], -["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20318,], -["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20318,], -["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20318,], -["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20318,], -["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20318,], -["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20318,], -["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20318,], +["features.messages.impl.topbars_MessagesViewTopBar_Day_0_en","features.messages.impl.topbars_MessagesViewTopBar_Night_0_en",20360,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20360,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20360,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20360,], +["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20360,], +["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20360,], +["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20360,], +["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20360,], +["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20360,], +["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20360,], +["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20360,], +["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20360,], +["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20360,], +["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20360,], +["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20360,], ["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",20318,], +["features.migration.impl_MigrationView_Day_1_en","features.migration.impl_MigrationView_Night_1_en",20360,], ["libraries.designsystem.theme.components_ModalBottomSheetDark_Bottom_Sheets_en","",0,], ["libraries.designsystem.theme.components_ModalBottomSheetLight_Bottom_Sheets_en","",0,], ["appicon.element_MonochromeIcon_en","",0,], +["features.preferences.impl.root_MultiAccountSection_Day_0_en","features.preferences.impl.root_MultiAccountSection_Night_0_en",20360,], ["libraries.designsystem.components.dialogs_MultipleSelectionDialogContent_Dialogs_en","",0,], ["libraries.designsystem.components.dialogs_MultipleSelectionDialog_Day_0_en","libraries.designsystem.components.dialogs_MultipleSelectionDialog_Night_0_en",0,], ["libraries.designsystem.components.list_MutipleSelectionListItemSelectedTrailingContent_Multiple_selection_List_item_-_selection_in_trailing_content_List_items_en","",0,], ["libraries.designsystem.components.list_MutipleSelectionListItemSelected_Multiple_selection_List_item_-_selection_in_supporting_text_List_items_en","",0,], ["libraries.designsystem.components.list_MutipleSelectionListItem_Multiple_selection_List_item_-_no_selection_List_items_en","",0,], ["libraries.designsystem.theme.components_NavigationBar_App_Bars_en","",0,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20318,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20318,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20318,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20318,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_13_en","features.preferences.impl.notifications_NotificationSettingsView_Night_13_en",20318,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20318,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20318,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20318,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20318,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20318,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20318,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20318,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20318,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20318,], -["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20318,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20360,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20360,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20360,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20360,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_13_en","features.preferences.impl.notifications_NotificationSettingsView_Night_13_en",20360,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20360,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20360,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20360,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20360,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20360,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20360,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20360,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20360,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20360,], +["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20360,], ["libraries.designsystem.atomic.pages_OnBoardingPage_Day_0_en","libraries.designsystem.atomic.pages_OnBoardingPage_Night_0_en",0,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_0_en","features.login.impl.screens.onboarding_OnBoardingView_Night_0_en",20318,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_1_en","features.login.impl.screens.onboarding_OnBoardingView_Night_1_en",20318,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_2_en","features.login.impl.screens.onboarding_OnBoardingView_Night_2_en",20318,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_3_en","features.login.impl.screens.onboarding_OnBoardingView_Night_3_en",20318,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_4_en","features.login.impl.screens.onboarding_OnBoardingView_Night_4_en",20318,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_5_en","features.login.impl.screens.onboarding_OnBoardingView_Night_5_en",20318,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_0_en","features.login.impl.screens.onboarding_OnBoardingView_Night_0_en",20360,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_1_en","features.login.impl.screens.onboarding_OnBoardingView_Night_1_en",20360,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_2_en","features.login.impl.screens.onboarding_OnBoardingView_Night_2_en",20360,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_3_en","features.login.impl.screens.onboarding_OnBoardingView_Night_3_en",20360,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_4_en","features.login.impl.screens.onboarding_OnBoardingView_Night_4_en",20360,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_5_en","features.login.impl.screens.onboarding_OnBoardingView_Night_5_en",20360,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_6_en","features.login.impl.screens.onboarding_OnBoardingView_Night_6_en",20360,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_7_en","features.login.impl.screens.onboarding_OnBoardingView_Night_7_en",20360,], ["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",20318,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en",20318,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en",20318,], +["libraries.matrix.ui.components_OrganizationHeader_Day_0_en","libraries.matrix.ui.components_OrganizationHeader_Night_0_en",20360,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en",20360,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en",20360,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en",20360,], ["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",20318,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en",20318,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en",20318,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en",20318,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en",20318,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en",20318,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en",20318,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en",20318,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en",20318,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_1_en",20360,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en",20360,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en",20360,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en",20360,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en",20360,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en",20360,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en",20360,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en",20360,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en",20360,], ["libraries.designsystem.theme.components_OutlinedButtonLargeLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonLarge_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonMediumLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonMedium_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonSmall_Buttons_en","",0,], -["libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en",20318,], -["features.changeroommemberroles.impl_PendingMemberRowWithLongName_Day_0_en","features.changeroommemberroles.impl_PendingMemberRowWithLongName_Night_0_en",20318,], -["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20318,], -["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20318,], -["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20318,], -["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20318,], +["libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en",20360,], +["features.changeroommemberroles.impl_PendingMemberRowWithLongName_Day_0_en","features.changeroommemberroles.impl_PendingMemberRowWithLongName_Night_0_en",20360,], +["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20360,], +["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20360,], +["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20360,], +["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20360,], ["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",20318,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20318,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20318,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20318,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20318,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20318,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20318,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20318,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20318,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20318,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20318,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20318,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20318,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20318,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20318,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20318,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20360,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20360,], ["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",20318,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20318,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20318,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20318,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20318,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20318,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20318,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20318,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20318,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20318,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20318,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20318,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20318,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20318,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_10_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_10_en",20360,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20360,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20360,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20360,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20360,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20360,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20360,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20360,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20360,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20360,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20360,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20360,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20360,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20360,], ["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",20318,], -["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20318,], -["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20318,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20318,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20318,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",20360,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20360,], +["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20360,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20360,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20360,], ["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",20318,], -["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20318,], -["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20318,], -["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20318,], -["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20318,], -["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20318,], -["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20318,], -["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20318,], -["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20318,], -["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20318,], -["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20318,], +["features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en",20360,], +["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20360,], +["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20360,], +["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20360,], +["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20360,], +["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20360,], +["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20360,], +["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20360,], +["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20360,], +["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20360,], +["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20360,], ["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,], @@ -842,204 +907,207 @@ export const screenshots = [ ["libraries.designsystem.components.preferences_PreferenceRow_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceSlide_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceSwitch_Preferences_en","",0,], -["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20318,], -["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20318,], -["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20318,], -["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20318,], +["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20360,], +["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20360,], +["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20360,], +["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20360,], ["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","",20318,], -["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20318,], -["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20318,], -["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20318,], -["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20318,], -["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20318,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en",20318,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en",20318,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en",20318,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20318,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20318,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20318,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20318,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20318,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20318,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20318,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20318,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20318,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20318,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20318,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20318,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20318,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20318,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20318,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20318,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en",20318,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_5_en",20318,], +["libraries.designsystem.components_ProgressDialogContent_Dialogs_en","",20360,], +["libraries.designsystem.components_ProgressDialogWithContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithContent_Night_0_en",20360,], +["libraries.designsystem.components_ProgressDialogWithTextAndContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithTextAndContent_Night_0_en",0,], +["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20360,], +["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20360,], +["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20360,], +["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20360,], +["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20360,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en",20360,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en",20360,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en",20360,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_3_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_3_en",20360,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20360,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20360,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20360,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20360,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20360,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20360,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20360,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20360,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20360,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20360,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20360,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20360,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20360,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20360,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20360,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20360,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en",20360,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_5_en",20360,], ["libraries.designsystem.theme.components_RadioButton_Toggles_en","",0,], -["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20318,], -["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20318,], +["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20360,], +["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20360,], ["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",20318,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20318,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20318,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20318,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20318,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_14_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_14_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20318,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20318,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en",20360,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20360,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20360,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20360,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20360,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_14_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_14_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20360,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20360,], ["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",20318,], -["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20318,], -["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20318,], -["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20318,], -["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20318,], -["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20318,], -["features.reportroom.impl_ReportRoomView_Day_0_en","features.reportroom.impl_ReportRoomView_Night_0_en",20318,], -["features.reportroom.impl_ReportRoomView_Day_1_en","features.reportroom.impl_ReportRoomView_Night_1_en",20318,], -["features.reportroom.impl_ReportRoomView_Day_2_en","features.reportroom.impl_ReportRoomView_Night_2_en",20318,], -["features.reportroom.impl_ReportRoomView_Day_3_en","features.reportroom.impl_ReportRoomView_Night_3_en",20318,], -["features.reportroom.impl_ReportRoomView_Day_4_en","features.reportroom.impl_ReportRoomView_Night_4_en",20318,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20318,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20318,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20318,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20318,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20318,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20318,], +["features.messages.impl.report_ReportMessageView_Day_0_en","features.messages.impl.report_ReportMessageView_Night_0_en",20360,], +["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20360,], +["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20360,], +["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20360,], +["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20360,], +["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20360,], +["features.reportroom.impl_ReportRoomView_Day_0_en","features.reportroom.impl_ReportRoomView_Night_0_en",20360,], +["features.reportroom.impl_ReportRoomView_Day_1_en","features.reportroom.impl_ReportRoomView_Night_1_en",20360,], +["features.reportroom.impl_ReportRoomView_Day_2_en","features.reportroom.impl_ReportRoomView_Night_2_en",20360,], +["features.reportroom.impl_ReportRoomView_Day_3_en","features.reportroom.impl_ReportRoomView_Night_3_en",20360,], +["features.reportroom.impl_ReportRoomView_Day_4_en","features.reportroom.impl_ReportRoomView_Night_4_en",20360,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20360,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20360,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20360,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20360,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20360,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20360,], ["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",20318,], -["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20318,], -["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20318,], -["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20318,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_0_en",20318,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_1_en",20318,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_2_en",20318,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_3_en",20318,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_4_en",20318,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_5_en",20318,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_6_en",20318,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_7_en",20318,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_8_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_8_en",20318,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en",20360,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20360,], +["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20360,], +["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20360,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_0_en",20360,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_1_en",20360,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_2_en",20360,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_3_en",20360,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_4_en",20360,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_5_en",20360,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_6_en",20360,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_7_en",20360,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_8_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_8_en",20360,], ["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",20318,], -["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20318,], -["features.roomdetails.impl_RoomDetailsDark_0_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_10_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_11_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_12_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_13_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_14_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_15_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_16_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_17_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_18_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_19_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_1_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_2_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_3_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_4_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_5_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_6_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_7_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_8_en","",20318,], -["features.roomdetails.impl_RoomDetailsDark_9_en","",20318,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_0_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_0_en",20318,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_1_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_1_en",20318,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_2_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_2_en",20318,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_3_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_3_en",20318,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_4_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_4_en",20318,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_5_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_5_en",20318,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_6_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_6_en",20318,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_7_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_7_en",20318,], -["features.roomdetails.impl_RoomDetails_0_en","",20318,], -["features.roomdetails.impl_RoomDetails_10_en","",20318,], -["features.roomdetails.impl_RoomDetails_11_en","",20318,], -["features.roomdetails.impl_RoomDetails_12_en","",20318,], -["features.roomdetails.impl_RoomDetails_13_en","",20318,], -["features.roomdetails.impl_RoomDetails_14_en","",20318,], -["features.roomdetails.impl_RoomDetails_15_en","",20318,], -["features.roomdetails.impl_RoomDetails_16_en","",20318,], -["features.roomdetails.impl_RoomDetails_17_en","",20318,], -["features.roomdetails.impl_RoomDetails_18_en","",20318,], -["features.roomdetails.impl_RoomDetails_19_en","",20318,], -["features.roomdetails.impl_RoomDetails_1_en","",20318,], -["features.roomdetails.impl_RoomDetails_2_en","",20318,], -["features.roomdetails.impl_RoomDetails_3_en","",20318,], -["features.roomdetails.impl_RoomDetails_4_en","",20318,], -["features.roomdetails.impl_RoomDetails_5_en","",20318,], -["features.roomdetails.impl_RoomDetails_6_en","",20318,], -["features.roomdetails.impl_RoomDetails_7_en","",20318,], -["features.roomdetails.impl_RoomDetails_8_en","",20318,], -["features.roomdetails.impl_RoomDetails_9_en","",20318,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20318,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20318,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20318,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20318,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20318,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20318,], -["features.home.impl.components_RoomListContentView_Day_0_en","features.home.impl.components_RoomListContentView_Night_0_en",20318,], -["features.home.impl.components_RoomListContentView_Day_1_en","features.home.impl.components_RoomListContentView_Night_1_en",20318,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en",20360,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20360,], +["features.roomdetails.impl_RoomDetailsDark_0_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_10_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_11_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_12_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_13_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_14_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_15_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_16_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_17_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_18_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_19_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_1_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_2_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_3_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_4_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_5_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_6_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_7_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_8_en","",20360,], +["features.roomdetails.impl_RoomDetailsDark_9_en","",20360,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_0_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_0_en",20360,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_1_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_1_en",20360,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_2_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_2_en",20360,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_3_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_3_en",20360,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_4_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_4_en",20360,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_5_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_5_en",20360,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_6_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_6_en",20360,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_7_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_7_en",20360,], +["features.roomdetails.impl_RoomDetails_0_en","",20360,], +["features.roomdetails.impl_RoomDetails_10_en","",20360,], +["features.roomdetails.impl_RoomDetails_11_en","",20360,], +["features.roomdetails.impl_RoomDetails_12_en","",20360,], +["features.roomdetails.impl_RoomDetails_13_en","",20360,], +["features.roomdetails.impl_RoomDetails_14_en","",20360,], +["features.roomdetails.impl_RoomDetails_15_en","",20360,], +["features.roomdetails.impl_RoomDetails_16_en","",20360,], +["features.roomdetails.impl_RoomDetails_17_en","",20360,], +["features.roomdetails.impl_RoomDetails_18_en","",20360,], +["features.roomdetails.impl_RoomDetails_19_en","",20360,], +["features.roomdetails.impl_RoomDetails_1_en","",20360,], +["features.roomdetails.impl_RoomDetails_2_en","",20360,], +["features.roomdetails.impl_RoomDetails_3_en","",20360,], +["features.roomdetails.impl_RoomDetails_4_en","",20360,], +["features.roomdetails.impl_RoomDetails_5_en","",20360,], +["features.roomdetails.impl_RoomDetails_6_en","",20360,], +["features.roomdetails.impl_RoomDetails_7_en","",20360,], +["features.roomdetails.impl_RoomDetails_8_en","",20360,], +["features.roomdetails.impl_RoomDetails_9_en","",20360,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20360,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20360,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20360,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20360,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20360,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20360,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20360,], +["features.home.impl.components_RoomListContentView_Day_0_en","features.home.impl.components_RoomListContentView_Night_0_en",20360,], +["features.home.impl.components_RoomListContentView_Day_1_en","features.home.impl.components_RoomListContentView_Night_1_en",20360,], ["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",20318,], -["features.home.impl.components_RoomListContentView_Day_4_en","features.home.impl.components_RoomListContentView_Night_4_en",20318,], -["features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_en","features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Night_0_en",20318,], -["features.home.impl.filters_RoomListFiltersView_Day_0_en","features.home.impl.filters_RoomListFiltersView_Night_0_en",20318,], -["features.home.impl.filters_RoomListFiltersView_Day_1_en","features.home.impl.filters_RoomListFiltersView_Night_1_en",20318,], -["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_0_en",20318,], -["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_1_en",20318,], -["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_2_en",20318,], +["features.home.impl.components_RoomListContentView_Day_3_en","features.home.impl.components_RoomListContentView_Night_3_en",20360,], +["features.home.impl.components_RoomListContentView_Day_4_en","features.home.impl.components_RoomListContentView_Night_4_en",20360,], +["features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_en","features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Night_0_en",20360,], +["features.home.impl.filters_RoomListFiltersView_Day_0_en","features.home.impl.filters_RoomListFiltersView_Night_0_en",20360,], +["features.home.impl.filters_RoomListFiltersView_Day_1_en","features.home.impl.filters_RoomListFiltersView_Night_1_en",20360,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_0_en",20360,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_1_en",20360,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_2_en",20360,], ["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",20318,], -["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en",20318,], -["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en",20318,], -["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en",20318,], -["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20318,], -["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20318,], -["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20318,], -["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20318,], -["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20318,], -["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",20318,], +["features.home.impl.search_RoomListSearchContent_Day_1_en","features.home.impl.search_RoomListSearchContent_Night_1_en",20360,], +["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en",20360,], +["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en",20360,], +["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en",20360,], +["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20360,], +["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20360,], +["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20360,], +["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20360,], +["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20360,], +["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",20360,], ["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",20318,], -["features.roomdetails.impl.members_RoomMemberListView_Day_8_en","features.roomdetails.impl.members_RoomMemberListView_Night_8_en",20318,], -["features.roomdetails.impl.members_RoomMemberListView_Day_9_en","features.roomdetails.impl.members_RoomMemberListView_Night_9_en",20318,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en",20318,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en",20318,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en",20318,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en",20318,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en",20318,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en",20318,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en",20318,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en",20318,], -["libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Day_0_en","libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Night_0_en",0,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20318,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20318,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20318,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20318,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20318,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20318,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20318,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20318,], -["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20318,], -["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20318,], -["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20318,], -["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20318,], -["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20318,], -["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20318,], +["features.roomdetails.impl.members_RoomMemberListView_Day_7_en","features.roomdetails.impl.members_RoomMemberListView_Night_7_en",20360,], +["features.roomdetails.impl.members_RoomMemberListView_Day_8_en","features.roomdetails.impl.members_RoomMemberListView_Night_8_en",20360,], +["features.roomdetails.impl.members_RoomMemberListView_Day_9_en","features.roomdetails.impl.members_RoomMemberListView_Night_9_en",20360,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en",20360,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en",20360,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en",20360,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en",20360,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en",20360,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en",20360,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en",20360,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en",20360,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20360,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20360,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20360,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20360,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20360,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20360,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20360,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20360,], +["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20360,], +["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20360,], +["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20360,], +["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20360,], +["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20360,], +["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20360,], ["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,], @@ -1062,13 +1130,13 @@ export const screenshots = [ ["features.home.impl.components_RoomSummaryRow_Day_26_en","features.home.impl.components_RoomSummaryRow_Night_26_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_27_en","features.home.impl.components_RoomSummaryRow_Night_27_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_28_en","features.home.impl.components_RoomSummaryRow_Night_28_en",0,], -["features.home.impl.components_RoomSummaryRow_Day_29_en","features.home.impl.components_RoomSummaryRow_Night_29_en",20318,], -["features.home.impl.components_RoomSummaryRow_Day_2_en","features.home.impl.components_RoomSummaryRow_Night_2_en",20318,], -["features.home.impl.components_RoomSummaryRow_Day_30_en","features.home.impl.components_RoomSummaryRow_Night_30_en",20318,], -["features.home.impl.components_RoomSummaryRow_Day_31_en","features.home.impl.components_RoomSummaryRow_Night_31_en",20318,], -["features.home.impl.components_RoomSummaryRow_Day_32_en","features.home.impl.components_RoomSummaryRow_Night_32_en",20318,], -["features.home.impl.components_RoomSummaryRow_Day_33_en","features.home.impl.components_RoomSummaryRow_Night_33_en",20318,], -["features.home.impl.components_RoomSummaryRow_Day_34_en","features.home.impl.components_RoomSummaryRow_Night_34_en",20318,], +["features.home.impl.components_RoomSummaryRow_Day_29_en","features.home.impl.components_RoomSummaryRow_Night_29_en",20360,], +["features.home.impl.components_RoomSummaryRow_Day_2_en","features.home.impl.components_RoomSummaryRow_Night_2_en",20360,], +["features.home.impl.components_RoomSummaryRow_Day_30_en","features.home.impl.components_RoomSummaryRow_Night_30_en",20360,], +["features.home.impl.components_RoomSummaryRow_Day_31_en","features.home.impl.components_RoomSummaryRow_Night_31_en",20360,], +["features.home.impl.components_RoomSummaryRow_Day_32_en","features.home.impl.components_RoomSummaryRow_Night_32_en",20360,], +["features.home.impl.components_RoomSummaryRow_Day_33_en","features.home.impl.components_RoomSummaryRow_Night_33_en",20360,], +["features.home.impl.components_RoomSummaryRow_Day_34_en","features.home.impl.components_RoomSummaryRow_Night_34_en",20360,], ["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,], @@ -1076,92 +1144,98 @@ export const screenshots = [ ["features.home.impl.components_RoomSummaryRow_Day_7_en","features.home.impl.components_RoomSummaryRow_Night_7_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_8_en","features.home.impl.components_RoomSummaryRow_Night_8_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_9_en","features.home.impl.components_RoomSummaryRow_Night_9_en",0,], -["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",20318,], -["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20318,], -["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20318,], +["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",20360,], +["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20360,], +["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20360,], ["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",20318,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20318,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20318,], +["features.verifysession.impl.emoji_SasEmojis_Day_0_en","features.verifysession.impl.emoji_SasEmojis_Night_0_en",20360,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20360,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20360,], ["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","",20318,], +["libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en","",20360,], ["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.startchat.impl.components_SearchMultipleUsersResultItem_en","",20318,], -["features.startchat.impl.components_SearchSingleUserResultItem_en","",20318,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20318,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20318,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20318,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20318,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20318,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20318,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20318,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20318,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_4_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20318,], -["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20318,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20318,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20318,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20318,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20318,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20318,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en",20318,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20318,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20318,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20318,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20318,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20318,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_3_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_7_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_8_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_3_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_7_en","",20318,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_8_en","",20318,], +["features.startchat.impl.components_SearchMultipleUsersResultItem_en","",20360,], +["features.startchat.impl.components_SearchSingleUserResultItem_en","",20360,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20360,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20360,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20360,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20360,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20360,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20360,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20360,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20360,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_4_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20360,], +["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20360,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20360,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20360,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20360,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20360,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20360,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en",20360,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20360,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20360,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20360,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20360,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20360,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_3_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_7_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_8_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_3_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_7_en","",20360,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_8_en","",20360,], +["libraries.designsystem.atomic.atoms_SelectedIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_SelectedIndicatorAtom_Night_0_en",0,], +["libraries.matrix.ui.components_SelectedRoomRtl_Day_0_en","libraries.matrix.ui.components_SelectedRoomRtl_Night_0_en",0,], +["libraries.matrix.ui.components_SelectedRoomRtl_Day_1_en","libraries.matrix.ui.components_SelectedRoomRtl_Night_1_en",0,], +["libraries.matrix.ui.components_SelectedRoomRtl_Day_2_en","libraries.matrix.ui.components_SelectedRoomRtl_Night_2_en",0,], ["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,], ["libraries.matrix.ui.components_SelectedUserCannotRemove_Day_0_en","libraries.matrix.ui.components_SelectedUserCannotRemove_Night_0_en",0,], +["libraries.matrix.ui.components_SelectedUserRtl_Day_0_en","libraries.matrix.ui.components_SelectedUserRtl_Night_0_en",0,], ["libraries.matrix.ui.components_SelectedUser_Day_0_en","libraries.matrix.ui.components_SelectedUser_Night_0_en",0,], +["libraries.matrix.ui.components_SelectedUser_Day_1_en","libraries.matrix.ui.components_SelectedUser_Night_1_en",0,], ["libraries.matrix.ui.components_SelectedUsersRowList_Day_0_en","libraries.matrix.ui.components_SelectedUsersRowList_Night_0_en",0,], ["libraries.textcomposer.components_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",20318,], -["features.location.impl.send_SendLocationView_Day_1_en","features.location.impl.send_SendLocationView_Night_1_en",20318,], -["features.location.impl.send_SendLocationView_Day_2_en","features.location.impl.send_SendLocationView_Night_2_en",20318,], -["features.location.impl.send_SendLocationView_Day_3_en","features.location.impl.send_SendLocationView_Night_3_en",20318,], -["features.location.impl.send_SendLocationView_Day_4_en","features.location.impl.send_SendLocationView_Night_4_en",20318,], +["features.location.impl.send_SendLocationView_Day_0_en","features.location.impl.send_SendLocationView_Night_0_en",20360,], +["features.location.impl.send_SendLocationView_Day_1_en","features.location.impl.send_SendLocationView_Night_1_en",20360,], +["features.location.impl.send_SendLocationView_Day_2_en","features.location.impl.send_SendLocationView_Night_2_en",20360,], +["features.location.impl.send_SendLocationView_Day_3_en","features.location.impl.send_SendLocationView_Night_3_en",20360,], +["features.location.impl.send_SendLocationView_Day_4_en","features.location.impl.send_SendLocationView_Night_4_en",20360,], ["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,], @@ -1171,27 +1245,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",20318,], -["features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20318,], -["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20318,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20318,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20318,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20318,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20318,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20318,], +["features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en","features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en",20360,], +["features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20360,], +["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20360,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20360,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20360,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20360,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20360,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20360,], ["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",20318,], -["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20318,], -["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20318,], -["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20318,], -["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20318,], -["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20318,], -["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20318,], -["features.location.impl.show_ShowLocationView_Day_6_en","features.location.impl.show_ShowLocationView_Night_6_en",20318,], -["features.location.impl.show_ShowLocationView_Day_7_en","features.location.impl.show_ShowLocationView_Night_7_en",20318,], -["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20318,], +["features.share.impl_ShareView_Day_3_en","features.share.impl_ShareView_Night_3_en",20360,], +["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20360,], +["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20360,], +["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20360,], +["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20360,], +["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20360,], +["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20360,], +["features.location.impl.show_ShowLocationView_Day_6_en","features.location.impl.show_ShowLocationView_Night_6_en",20360,], +["features.location.impl.show_ShowLocationView_Day_7_en","features.location.impl.show_ShowLocationView_Night_7_en",20360,], +["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20360,], ["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,], @@ -1200,77 +1274,86 @@ 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",20318,], +["features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en","features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en",20360,], ["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.matrix.ui.components_SpaceHeaderRootView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderRootView_Night_0_en",20360,], +["libraries.matrix.ui.components_SpaceHeaderView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderView_Night_0_en",20360,], +["libraries.matrix.ui.components_SpaceInfoRow_Day_0_en","libraries.matrix.ui.components_SpaceInfoRow_Night_0_en",20360,], +["libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Day_0_en","libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Night_0_en",0,], +["libraries.matrix.ui.components_SpaceMembersView_Day_0_en","libraries.matrix.ui.components_SpaceMembersView_Night_0_en",0,], +["features.space.impl.root_SpaceView_Day_0_en","features.space.impl.root_SpaceView_Night_0_en",0,], +["features.space.impl.root_SpaceView_Day_1_en","features.space.impl.root_SpaceView_Night_1_en",20360,], +["features.space.impl.root_SpaceView_Day_2_en","features.space.impl.root_SpaceView_Night_2_en",20360,], +["features.space.impl.root_SpaceView_Day_3_en","features.space.impl.root_SpaceView_Night_3_en",20360,], ["libraries.designsystem.modifiers_SquareSizeModifierInsideSquare_en","",0,], ["libraries.designsystem.modifiers_SquareSizeModifierLargeHeight_en","",0,], ["libraries.designsystem.modifiers_SquareSizeModifierLargeWidth_en","",0,], -["features.startchat.impl.root_StartChatView_Day_0_en","features.startchat.impl.root_StartChatView_Night_0_en",20318,], -["features.startchat.impl.root_StartChatView_Day_1_en","features.startchat.impl.root_StartChatView_Night_1_en",20318,], -["features.startchat.impl.root_StartChatView_Day_2_en","features.startchat.impl.root_StartChatView_Night_2_en",20318,], -["features.startchat.impl.root_StartChatView_Day_3_en","features.startchat.impl.root_StartChatView_Night_3_en",20318,], -["features.startchat.impl.root_StartChatView_Day_4_en","features.startchat.impl.root_StartChatView_Night_4_en",20318,], -["features.startchat.impl.root_StartChatView_Day_5_en","features.startchat.impl.root_StartChatView_Night_5_en",20318,], -["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",20318,], +["features.startchat.impl.root_StartChatView_Day_0_en","features.startchat.impl.root_StartChatView_Night_0_en",20360,], +["features.startchat.impl.root_StartChatView_Day_1_en","features.startchat.impl.root_StartChatView_Night_1_en",20360,], +["features.startchat.impl.root_StartChatView_Day_2_en","features.startchat.impl.root_StartChatView_Night_2_en",20360,], +["features.startchat.impl.root_StartChatView_Day_3_en","features.startchat.impl.root_StartChatView_Night_3_en",20360,], +["features.startchat.impl.root_StartChatView_Day_4_en","features.startchat.impl.root_StartChatView_Night_4_en",20360,], +["features.startchat.impl.root_StartChatView_Day_5_en","features.startchat.impl.root_StartChatView_Night_5_en",20360,], +["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",20360,], ["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",20318,], +["features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en","features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en",20360,], ["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",20318,], +["appnav.loggedin_SyncStateView_Day_0_en","appnav.loggedin_SyncStateView_Night_0_en",20360,], ["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",20318,], -["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20318,], -["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20318,], -["libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en",20318,], -["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20318,], -["libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en",20318,], -["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20318,], -["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20318,], -["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20318,], -["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20318,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en",20318,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en",20318,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en",20318,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en",20318,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en",20318,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en",20318,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en",20318,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en",20318,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en",20318,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en",20318,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en",20318,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en",20318,], -["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20318,], -["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20318,], -["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20318,], -["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20318,], -["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20318,], -["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20318,], -["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20318,], -["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20318,], -["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20318,], -["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20318,], -["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20318,], -["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20318,], -["libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en",20318,], -["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20318,], -["libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en",20318,], +["libraries.textcomposer_TextComposerAddCaption_Day_0_en","libraries.textcomposer_TextComposerAddCaption_Night_0_en",20360,], +["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20360,], +["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20360,], +["libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en",20360,], +["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20360,], +["libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en",20360,], +["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20360,], +["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20360,], +["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20360,], +["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20360,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en",20360,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en",20360,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en",20360,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en",20360,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en",20360,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en",20360,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en",20360,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en",20360,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en",20360,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en",20360,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en",20360,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en",20360,], +["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20360,], +["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20360,], +["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20360,], +["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20360,], +["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20360,], +["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20360,], +["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20360,], +["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20360,], +["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20360,], +["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20360,], +["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20360,], +["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20360,], +["libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en",20360,], +["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20360,], +["libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en",20360,], ["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",20318,], -["libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en",20318,], +["libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialogWithError_Night_0_en",20360,], +["libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en",20360,], ["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,], @@ -1282,14 +1365,16 @@ export const screenshots = [ ["libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_3_en","libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_3_en",0,], ["libraries.textcomposer.components_TextFormatting_Day_0_en","libraries.textcomposer.components_TextFormatting_Night_0_en",0,], ["libraries.designsystem.theme.components_TextLight_Text_en","",0,], -["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20318,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20318,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20318,], +["features.messages.impl.timeline.components_ThreadSummaryView_Day_0_en","features.messages.impl.timeline.components_ThreadSummaryView_Night_0_en",20360,], +["features.messages.impl.topbars_ThreadTopBar_Day_0_en","features.messages.impl.topbars_ThreadTopBar_Night_0_en",20360,], +["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20360,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20360,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20360,], ["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",20318,], -["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20318,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en",20360,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20360,], ["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,], @@ -1299,18 +1384,18 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_2_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en",0,], -["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",20318,], +["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",20360,], ["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",20318,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20318,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20318,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20318,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20318,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20318,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20318,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20318,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en",20318,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_0_en",20360,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20360,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20360,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20360,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20360,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20360,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20360,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20360,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en",20360,], ["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,], @@ -1318,18 +1403,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",20318,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20318,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en",20360,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20360,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_6_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_6_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en",20318,], -["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20318,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20318,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en",20360,], +["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20360,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20360,], ["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",20318,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20318,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en",20360,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20360,], ["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,], @@ -1338,40 +1423,41 @@ 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",20318,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en",20360,], ["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",20318,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en",20360,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en",0,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Night_0_en",20360,], ["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","",20318,], +["features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en","",20360,], ["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",20318,], -["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20318,], -["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20318,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en",20360,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20360,], +["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20360,], ["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",20318,], +["features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en",20360,], ["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",20318,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20318,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20318,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20318,], -["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20318,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20360,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20360,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20360,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20360,], +["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20360,], ["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",20318,], -["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20318,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en",20360,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20360,], ["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",20318,], +["features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en",20360,], ["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,], @@ -1380,8 +1466,8 @@ export const screenshots = [ ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_5_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_5_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_6_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_6_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_7_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_7_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",20318,], -["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20318,], +["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",20360,], +["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20360,], ["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,], @@ -1396,8 +1482,8 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_4_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_5_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",20318,], -["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20318,], +["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",20360,], +["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20360,], ["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,], @@ -1420,85 +1506,85 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemVoiceView_Day_9_en","features.messages.impl.timeline.components.event_TimelineItemVoiceView_Night_9_en",0,], ["features.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en","features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en",0,], -["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",20318,], -["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20318,], +["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",20360,], +["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20360,], ["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",20318,], -["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20318,], -["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20318,], -["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20318,], -["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20318,], -["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20318,], -["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",20318,], -["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20318,], +["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20360,], +["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20360,], +["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20360,], +["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20360,], +["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20360,], +["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20360,], +["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",20360,], +["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20360,], ["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",20318,], +["features.messages.impl.timeline_TimelineView_Day_4_en","features.messages.impl.timeline_TimelineView_Night_4_en",20360,], ["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",20318,], +["features.messages.impl.timeline_TimelineView_Day_6_en","features.messages.impl.timeline_TimelineView_Night_6_en",20360,], ["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",20318,], +["features.messages.impl.timeline_TimelineView_Day_8_en","features.messages.impl.timeline_TimelineView_Night_8_en",20360,], ["features.messages.impl.timeline_TimelineView_Day_9_en","features.messages.impl.timeline_TimelineView_Night_9_en",0,], ["libraries.designsystem.components.avatar.internal_TombstonedRoomAvatar_Avatars_en","",0,], ["libraries.designsystem.theme.components_TopAppBarStr_App_Bars_en","",0,], ["libraries.designsystem.theme.components_TopAppBar_App_Bars_en","",0,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",20318,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20318,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20318,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20318,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20318,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20318,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20318,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20318,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",20360,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20360,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20360,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20360,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20360,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20360,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20360,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20360,], ["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",20318,], -["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20318,], -["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20318,], -["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20318,], -["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20318,], -["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20318,], +["features.messages.impl.typing_TypingNotificationView_Day_1_en","features.messages.impl.typing_TypingNotificationView_Night_1_en",20360,], +["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20360,], +["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20360,], +["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20360,], +["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20360,], +["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20360,], ["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","",20318,], +["libraries.matrix.ui.components_UnresolvedUserRow_en","",20360,], ["libraries.matrix.ui.components_UnsavedAvatar_Day_0_en","libraries.matrix.ui.components_UnsavedAvatar_Night_0_en",0,], ["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",20318,], -["features.startchat.impl.components_UserListView_Day_0_en","features.startchat.impl.components_UserListView_Night_0_en",20318,], -["features.startchat.impl.components_UserListView_Day_1_en","features.startchat.impl.components_UserListView_Night_1_en",20318,], -["features.startchat.impl.components_UserListView_Day_2_en","features.startchat.impl.components_UserListView_Night_2_en",20318,], +["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",20360,], +["features.startchat.impl.components_UserListView_Day_0_en","features.startchat.impl.components_UserListView_Night_0_en",20360,], +["features.startchat.impl.components_UserListView_Day_1_en","features.startchat.impl.components_UserListView_Night_1_en",20360,], +["features.startchat.impl.components_UserListView_Day_2_en","features.startchat.impl.components_UserListView_Night_2_en",20360,], ["features.startchat.impl.components_UserListView_Day_3_en","features.startchat.impl.components_UserListView_Night_3_en",0,], ["features.startchat.impl.components_UserListView_Day_4_en","features.startchat.impl.components_UserListView_Night_4_en",0,], ["features.startchat.impl.components_UserListView_Day_5_en","features.startchat.impl.components_UserListView_Night_5_en",0,], ["features.startchat.impl.components_UserListView_Day_6_en","features.startchat.impl.components_UserListView_Night_6_en",0,], -["features.startchat.impl.components_UserListView_Day_7_en","features.startchat.impl.components_UserListView_Night_7_en",20318,], +["features.startchat.impl.components_UserListView_Day_7_en","features.startchat.impl.components_UserListView_Night_7_en",20360,], ["features.startchat.impl.components_UserListView_Day_8_en","features.startchat.impl.components_UserListView_Night_8_en",0,], -["features.startchat.impl.components_UserListView_Day_9_en","features.startchat.impl.components_UserListView_Night_9_en",20318,], +["features.startchat.impl.components_UserListView_Day_9_en","features.startchat.impl.components_UserListView_Night_9_en",20360,], ["features.preferences.impl.user_UserPreferences_Day_0_en","features.preferences.impl.user_UserPreferences_Night_0_en",0,], ["features.preferences.impl.user_UserPreferences_Day_1_en","features.preferences.impl.user_UserPreferences_Night_1_en",0,], ["features.preferences.impl.user_UserPreferences_Day_2_en","features.preferences.impl.user_UserPreferences_Night_2_en",0,], -["features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en","features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en",20318,], -["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20318,], -["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20318,], -["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20318,], -["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20318,], -["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20318,], -["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20318,], -["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20318,], -["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20318,], -["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20318,], -["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",20318,], -["features.userprofile.shared_UserProfileView_Day_9_en","features.userprofile.shared_UserProfileView_Night_9_en",20318,], +["features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en","features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en",20360,], +["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20360,], +["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20360,], +["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20360,], +["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20360,], +["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20360,], +["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20360,], +["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20360,], +["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20360,], +["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20360,], +["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",20360,], +["features.userprofile.shared_UserProfileView_Day_9_en","features.userprofile.shared_UserProfileView_Night_9_en",20360,], ["features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en","features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en",0,], ["libraries.designsystem.ruler_VerticalRuler_Day_0_en","libraries.designsystem.ruler_VerticalRuler_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en",0,], -["features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_en","features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Night_0_en",20318,], -["features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_en","features.preferences.impl.advanced_VideoQualitySelectorDialog_Night_0_en",20318,], +["features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_en","features.preferences.impl.advanced_VideoQualitySelectorDialog_Night_0_en",20360,], +["features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_en","features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Night_0_en",20360,], ["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",20318,], +["features.viewfolder.impl.file_ViewFileView_Day_3_en","features.viewfolder.impl.file_ViewFileView_Night_3_en",20360,], ["features.viewfolder.impl.file_ViewFileView_Day_4_en","features.viewfolder.impl.file_ViewFileView_Night_4_en",0,], ["features.viewfolder.impl.file_ViewFileView_Day_5_en","features.viewfolder.impl.file_ViewFileView_Night_5_en",0,], ["features.viewfolder.impl.folder_ViewFolderView_Day_0_en","features.viewfolder.impl.folder_ViewFolderView_Night_0_en",0,], diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsService.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsService.kt index 750a6d1d17..589abf28fe 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 @@ -47,9 +47,4 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker { * Update analyticsId from the AccountData. */ suspend fun setAnalyticsId(analyticsId: String) - - /** - * Reset the analytics service (will ask for user consent again). - */ - suspend fun reset() } diff --git a/services/analytics/impl/build.gradle.kts b/services/analytics/impl/build.gradle.kts index 3ccd7a18c8..83eebead5d 100644 --- a/services/analytics/impl/build.gradle.kts +++ b/services/analytics/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -16,23 +17,21 @@ android { namespace = "io.element.android.services.analytics.impl" } -setupAnvil() +setupDependencyInjection() dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.designsystem) + implementation(projects.libraries.preferences.api) implementation(projects.libraries.sessionStorage.api) api(projects.services.analyticsproviders.api) api(projects.services.analytics.api) implementation(libs.androidx.datastore.preferences) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) + testCommonDependencies(libs) testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.services.analyticsproviders.test) - testImplementation(projects.tests.testutils) } 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 31d52d2249..04f0f6867a 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 @@ -7,14 +7,17 @@ package io.element.android.services.analytics.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metro.binding import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsScreen 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.SessionStore 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 @@ -27,17 +30,18 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean -import javax.inject.Inject @SingleIn(AppScope::class) -@ContributesBinding(AppScope::class, boundType = AnalyticsService::class, rank = ContributesBinding.RANK_HIGHEST) -class DefaultAnalyticsService @Inject constructor( +@ContributesBinding(AppScope::class, binding = binding()) +@Inject +class DefaultAnalyticsService( private val analyticsProviders: Set<@JvmSuppressWildcards AnalyticsProvider>, private val analyticsStore: AnalyticsStore, // private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory, @AppCoroutineScope private val coroutineScope: CoroutineScope, private val sessionObserver: SessionObserver, + private val sessionStore: SessionStore, ) : AnalyticsService, SessionListener { // Cache for the store values private val userConsent = AtomicBoolean(false) @@ -68,10 +72,6 @@ class DefaultAnalyticsService @Inject constructor( analyticsStore.setDidAskUserConsent() } - override suspend fun reset() { - analyticsStore.setDidAskUserConsent(false) - } - override suspend fun setAnalyticsId(analyticsId: String) { Timber.tag(analyticsTag.value).d("setAnalyticsId($analyticsId)") analyticsStore.setAnalyticsId(analyticsId) @@ -82,8 +82,10 @@ class DefaultAnalyticsService @Inject constructor( } override suspend fun onSessionDeleted(userId: String) { - // Delete the store - analyticsStore.reset() + // Delete the store when the last session is deleted + if (sessionStore.getAllSessions().isEmpty()) { + analyticsStore.reset() + } } private fun observeUserConsent() { diff --git a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultScreenTracker.kt b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultScreenTracker.kt index ad80decdbe..78ff13e554 100644 --- a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultScreenTracker.kt +++ b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultScreenTracker.kt @@ -13,17 +13,18 @@ import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.lifecycle.Lifecycle -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.libraries.designsystem.utils.OnLifecycleEvent -import io.element.android.libraries.di.AppScope import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.ScreenTracker import io.element.android.services.toolbox.api.systemclock.SystemClock -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultScreenTracker @Inject constructor( +@Inject +class DefaultScreenTracker( private val analyticsService: AnalyticsService, private val systemClock: SystemClock ) : ScreenTracker { diff --git a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/AnalyticsStore.kt b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/AnalyticsStore.kt index 7c7109e482..b6e1773be6 100644 --- a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/AnalyticsStore.kt +++ b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/AnalyticsStore.kt @@ -7,26 +7,17 @@ package io.element.android.services.analytics.impl.store -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import javax.inject.Inject - -/** - * Also accessed via reflection by the instrumentation tests @see [im.vector.app.ClearCurrentSessionRule]. - */ -private val Context.dataStore: DataStore by preferencesDataStore(name = "vector_analytics") /** * Local storage for: @@ -45,45 +36,48 @@ interface AnalyticsStore { } @ContributesBinding(AppScope::class) -class DefaultAnalyticsStore @Inject constructor( - @ApplicationContext private val context: Context +@Inject +class DefaultAnalyticsStore( + preferenceDataStoreFactory: PreferenceDataStoreFactory, ) : AnalyticsStore { private val userConsent = booleanPreferencesKey("user_consent") private val didAskUserConsent = booleanPreferencesKey("did_ask_user_consent") private val analyticsId = stringPreferencesKey("analytics_id") - override val userConsentFlow: Flow = context.dataStore.data + private val dataStore = preferenceDataStoreFactory.create("vector_analytics") + + override val userConsentFlow: Flow = dataStore.data .map { preferences -> preferences[userConsent].orFalse() } .distinctUntilChanged() - override val didAskUserConsentFlow: Flow = context.dataStore.data + override val didAskUserConsentFlow: Flow = dataStore.data .map { preferences -> preferences[didAskUserConsent].orFalse() } .distinctUntilChanged() - override val analyticsIdFlow: Flow = context.dataStore.data + override val analyticsIdFlow: Flow = dataStore.data .map { preferences -> preferences[analyticsId].orEmpty() } .distinctUntilChanged() override suspend fun setUserConsent(newUserConsent: Boolean) { - context.dataStore.edit { settings -> + dataStore.edit { settings -> settings[userConsent] = newUserConsent } } override suspend fun setDidAskUserConsent(newValue: Boolean) { - context.dataStore.edit { settings -> + dataStore.edit { settings -> settings[didAskUserConsent] = newValue } } override suspend fun setAnalyticsId(newAnalyticsId: String) { - context.dataStore.edit { settings -> + dataStore.edit { settings -> settings[analyticsId] = newAnalyticsId } } override suspend fun reset() { - context.dataStore.edit { + dataStore.edit { it.clear() } } 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 e05a6e4208..1e5a54fb26 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 @@ -16,7 +16,9 @@ import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.PollEnd import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.features.analytics.plan.UserProperties +import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.observer.SessionObserver +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver import io.element.android.services.analytics.impl.store.AnalyticsStore import io.element.android.services.analytics.impl.store.FakeAnalyticsStore @@ -167,7 +169,7 @@ class DefaultAnalyticsServiceTest { } @Test - fun `when a session is deleted, the store is reset`() = runTest { + fun `when the last session is deleted, the store is reset`() = runTest { val resetLambda = lambdaRecorder { } val store = FakeAnalyticsStore( resetLambda = resetLambda, @@ -180,20 +182,6 @@ class DefaultAnalyticsServiceTest { resetLambda.assertions().isCalledOnce() } - @Test - fun `when reset is invoked, the user consent is reset`() = runTest { - val store = FakeAnalyticsStore( - defaultDidAskUserConsent = true, - ) - val sut = createDefaultAnalyticsService( - coroutineScope = backgroundScope, - analyticsStore = store, - ) - assertThat(store.didAskUserConsentFlow.first()).isTrue() - sut.reset() - assertThat(store.didAskUserConsentFlow.first()).isFalse() - } - @Test fun `when a session is added, nothing happen`() = runTest { val sut = createDefaultAnalyticsService( @@ -272,11 +260,13 @@ class DefaultAnalyticsServiceTest { ), analyticsStore: AnalyticsStore = FakeAnalyticsStore(), sessionObserver: SessionObserver = NoOpSessionObserver(), + sessionStore: SessionStore = InMemorySessionStore(), ) = DefaultAnalyticsService( analyticsProviders = analyticsProviders, analyticsStore = analyticsStore, coroutineScope = coroutineScope, sessionObserver = sessionObserver, + sessionStore = sessionStore, ).also { // Wait for the service to be ready delay(1) diff --git a/services/analytics/noop/build.gradle.kts b/services/analytics/noop/build.gradle.kts index a4255733f1..cd6d16d029 100644 --- a/services/analytics/noop/build.gradle.kts +++ b/services/analytics/noop/build.gradle.kts @@ -1,4 +1,4 @@ -import extension.setupAnvil +import extension.setupDependencyInjection /* * Copyright 2023, 2024 New Vector Ltd. @@ -14,10 +14,9 @@ android { namespace = "io.element.android.services.analytics.noop" } -setupAnvil() +setupDependencyInjection() dependencies { - implementation(libs.dagger) implementation(projects.libraries.architecture) implementation(projects.libraries.di) api(projects.services.analytics.api) 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 9af15543f0..db03ca5553 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 @@ -7,22 +7,23 @@ package io.element.android.services.analytics.noop -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsScreen 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.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.AnalyticsProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class NoopAnalyticsService @Inject constructor() : AnalyticsService { +@Inject +class NoopAnalyticsService : AnalyticsService { override fun getAvailableAnalyticsProviders(): Set = emptySet() override val userConsentFlow: Flow = flowOf(false) override suspend fun setUserConsent(userConsent: Boolean) = Unit @@ -30,7 +31,6 @@ class NoopAnalyticsService @Inject constructor() : AnalyticsService { override suspend fun setDidAskUserConsent() = Unit override val analyticsIdFlow: Flow = flowOf("") override suspend fun setAnalyticsId(analyticsId: String) = Unit - override suspend fun reset() = Unit override fun capture(event: VectorAnalyticsEvent) = Unit override fun screen(screen: VectorAnalyticsScreen) = Unit override fun updateUserProperties(userProperties: UserProperties) = Unit diff --git a/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/NoopScreenTracker.kt b/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/NoopScreenTracker.kt index 6ddb8676a2..fb193e115d 100644 --- a/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/NoopScreenTracker.kt +++ b/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/NoopScreenTracker.kt @@ -8,14 +8,15 @@ package io.element.android.services.analytics.noop import androidx.compose.runtime.Composable -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.libraries.di.AppScope import io.element.android.services.analytics.api.ScreenTracker -import javax.inject.Inject @ContributesBinding(AppScope::class) -class NoopScreenTracker @Inject constructor() : ScreenTracker { +@Inject +class NoopScreenTracker : ScreenTracker { @Composable override fun TrackScreen(screen: MobileScreen.ScreenName) = 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 081f66d4e6..f8b250e37b 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 @@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.asStateFlow class FakeAnalyticsService( isEnabled: Boolean = false, didAskUserConsent: Boolean = false, - private val resetLambda: () -> Unit = {}, ) : AnalyticsService { private val isEnabledFlow = MutableStateFlow(isEnabled) override val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent) @@ -65,9 +64,4 @@ class FakeAnalyticsService( override fun updateSuperProperties(updatedProperties: SuperProperties) { // No op } - - override suspend fun reset() { - didAskUserConsentFlow.value = false - resetLambda() - } } diff --git a/services/analyticsproviders/posthog/build.gradle.kts b/services/analyticsproviders/posthog/build.gradle.kts index 01e6dbc836..ffcb2ba590 100644 --- a/services/analyticsproviders/posthog/build.gradle.kts +++ b/services/analyticsproviders/posthog/build.gradle.kts @@ -1,6 +1,7 @@ import config.BuildTimeConfig import extension.buildConfigFieldStr -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2023, 2024 New Vector Ltd. @@ -31,10 +32,9 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { - implementation(libs.dagger) implementation(libs.posthog) { exclude("com.android.support", "support-annotations") } @@ -43,9 +43,5 @@ dependencies { implementation(projects.libraries.di) implementation(projects.services.analyticsproviders.api) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.truth) - testImplementation(libs.test.junit) - testImplementation(projects.tests.testutils) - testImplementation(libs.test.mockk) + testCommonDependencies(libs) } 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 af450cfc84..3cc842817e 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,11 +11,12 @@ import android.content.Context import com.posthog.PostHogInterface import com.posthog.android.PostHogAndroid import com.posthog.android.PostHogAndroidConfig +import dev.zacsweers.metro.Inject import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.di.ApplicationContext -import javax.inject.Inject +import io.element.android.libraries.di.annotations.ApplicationContext -class PostHogFactory @Inject constructor( +@Inject +class PostHogFactory( @ApplicationContext private val context: Context, private val buildMeta: BuildMeta, private val posthogEndpointConfigProvider: PosthogEndpointConfigProvider, diff --git a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogAnalyticsProvider.kt b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogAnalyticsProvider.kt index 8d2b61d62c..a8cfcc886b 100644 --- a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogAnalyticsProvider.kt +++ b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogAnalyticsProvider.kt @@ -8,22 +8,23 @@ package io.element.android.services.analyticsproviders.posthog import com.posthog.PostHogInterface -import com.squareup.anvil.annotations.ContributesMultibinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsScreen 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.services.analyticsproviders.api.AnalyticsProvider import io.element.android.services.analyticsproviders.posthog.log.analyticsTag import timber.log.Timber -import javax.inject.Inject // private val REUSE_EXISTING_ID: String? = null // private val IGNORED_OPTIONS: Options? = null -@ContributesMultibinding(AppScope::class) -class PosthogAnalyticsProvider @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class PosthogAnalyticsProvider( private val postHogFactory: PostHogFactory, ) : AnalyticsProvider { override val name = "Posthog" 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 da35d661e5..67705d0a45 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,13 +7,14 @@ package io.element.android.services.analyticsproviders.posthog +import dev.zacsweers.metro.Inject 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( +@Inject +class PosthogEndpointConfigProvider( private val buildMeta: BuildMeta, private val enterpriseService: EnterpriseService, ) { diff --git a/services/analyticsproviders/sentry/build.gradle.kts b/services/analyticsproviders/sentry/build.gradle.kts index 1b1c351712..149ffb55dd 100644 --- a/services/analyticsproviders/sentry/build.gradle.kts +++ b/services/analyticsproviders/sentry/build.gradle.kts @@ -1,7 +1,7 @@ import config.BuildTimeConfig import extension.buildConfigFieldStr import extension.readLocalProperty -import extension.setupAnvil +import extension.setupDependencyInjection /* * Copyright 2023, 2024 New Vector Ltd. @@ -34,10 +34,9 @@ android { } } -setupAnvil() +setupDependencyInjection() dependencies { - implementation(libs.dagger) implementation(libs.sentry) implementation(projects.libraries.core) implementation(projects.libraries.di) 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 0effa8dc41..c83d756b30 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 @@ -8,15 +8,16 @@ package io.element.android.services.analyticsproviders.sentry import android.content.Context -import com.squareup.anvil.annotations.ContributesMultibinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoSet +import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsScreen import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.features.analytics.plan.UserProperties 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.di.annotations.ApplicationContext import io.element.android.services.analyticsproviders.api.AnalyticsProvider import io.element.android.services.analyticsproviders.sentry.log.analyticsTag import io.sentry.Breadcrumb @@ -24,12 +25,10 @@ import io.sentry.Sentry import io.sentry.SentryOptions import io.sentry.android.core.SentryAndroid import timber.log.Timber -import javax.inject.Inject -import kotlin.collections.component1 -import kotlin.collections.component2 -@ContributesMultibinding(AppScope::class) -class SentryAnalyticsProvider @Inject constructor( +@ContributesIntoSet(AppScope::class) +@Inject +class SentryAnalyticsProvider( @ApplicationContext private val context: Context, private val buildMeta: BuildMeta, ) : AnalyticsProvider { diff --git a/services/apperror/impl/build.gradle.kts b/services/apperror/impl/build.gradle.kts index 01163d240c..673468e99a 100644 --- a/services/apperror/impl/build.gradle.kts +++ b/services/apperror/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -11,29 +12,24 @@ plugins { id("io.element.android-compose-library") } -setupAnvil() +setupDependencyInjection() android { namespace = "io.element.android.services.apperror.impl" } dependencies { - implementation(libs.dagger) implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) implementation(projects.services.toolbox.api) - implementation(projects.anvilannotations) implementation(libs.coroutines.core) implementation(libs.androidx.corektx) api(projects.services.apperror.api) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.turbine) - testImplementation(libs.test.truth) + testCommonDependencies(libs) testImplementation(projects.services.toolbox.test) } diff --git a/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateService.kt b/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateService.kt index 9f9ede50ee..7d4dc8d64e 100644 --- a/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateService.kt +++ b/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateService.kt @@ -7,19 +7,20 @@ package io.element.android.services.apperror.impl -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.element.android.services.apperror.api.AppErrorState import io.element.android.services.apperror.api.AppErrorStateService import io.element.android.services.toolbox.api.strings.StringProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) -class DefaultAppErrorStateService @Inject constructor( +@Inject +class DefaultAppErrorStateService( private val stringProvider: StringProvider, ) : AppErrorStateService { private val currentAppErrorState = MutableStateFlow(AppErrorState.NoError) diff --git a/services/appnavstate/api/build.gradle.kts b/services/appnavstate/api/build.gradle.kts index cba388bfbd..f25cf59764 100644 --- a/services/appnavstate/api/build.gradle.kts +++ b/services/appnavstate/api/build.gradle.kts @@ -1,3 +1,5 @@ +import extension.setupDependencyInjection + /* * Copyright 2022-2024 New Vector Ltd. * @@ -13,6 +15,8 @@ android { namespace = "io.element.android.services.appnavstate.api" } +setupDependencyInjection() + dependencies { implementation(libs.coroutines.core) implementation(libs.androidx.lifecycle.runtime) 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 index c45fb0a280..07050d7bea 100644 --- 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 @@ -7,19 +7,20 @@ package io.element.android.services.appnavstate.api -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.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() { +@Inject +class ActiveRoomsHolder { private val rooms = ConcurrentHashMap>() /** diff --git a/services/appnavstate/impl/build.gradle.kts b/services/appnavstate/impl/build.gradle.kts index 6a12051df8..ba58e96f69 100644 --- a/services/appnavstate/impl/build.gradle.kts +++ b/services/appnavstate/impl/build.gradle.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright 2022-2024 New Vector Ltd. @@ -11,14 +12,13 @@ plugins { id("io.element.android-library") } -setupAnvil() +setupDependencyInjection() android { namespace = "io.element.android.services.appnavstate.impl" } dependencies { - implementation(libs.dagger) implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) @@ -29,10 +29,7 @@ dependencies { api(projects.services.appnavstate.api) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.test.truth) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.tests.testutils) testImplementation(projects.services.appnavstate.test) } 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 204ab66495..013ce5459f 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 @@ -7,10 +7,11 @@ package io.element.android.services.appnavstate.impl -import com.squareup.anvil.annotations.ContributesBinding +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn 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 @@ -26,7 +27,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject private val loggerTag = LoggerTag("Navigation") @@ -35,7 +35,8 @@ private val loggerTag = LoggerTag("Navigation") */ @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) -class DefaultAppNavigationStateService @Inject constructor( +@Inject +class DefaultAppNavigationStateService( private val appForegroundStateService: AppForegroundStateService, @AppCoroutineScope coroutineScope: CoroutineScope, diff --git a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/di/AppNavStateModule.kt b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/di/AppNavStateModule.kt index 4a78f1fdcf..326d4de059 100644 --- a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/di/AppNavStateModule.kt +++ b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/di/AppNavStateModule.kt @@ -9,15 +9,15 @@ package io.element.android.services.appnavstate.impl.di import android.content.Context import androidx.startup.AppInitializer -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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.services.appnavstate.api.AppForegroundStateService import io.element.android.services.appnavstate.impl.initializer.AppForegroundStateServiceInitializer -@Module +@BindingContainer @ContributesTo(AppScope::class) object AppNavStateModule { @Provides diff --git a/services/toolbox/impl/build.gradle.kts b/services/toolbox/impl/build.gradle.kts index d99efee5c0..f436771591 100644 --- a/services/toolbox/impl/build.gradle.kts +++ b/services/toolbox/impl/build.gradle.kts @@ -1,4 +1,4 @@ -import extension.setupAnvil +import extension.setupDependencyInjection /* * Copyright 2023, 2024 New Vector Ltd. @@ -14,10 +14,9 @@ android { namespace = "io.element.android.services.toolbox.impl" } -setupAnvil() +setupDependencyInjection() dependencies { - implementation(libs.dagger) implementation(projects.libraries.androidutils) implementation(projects.libraries.di) api(projects.services.toolbox.api) diff --git a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/intent/DefaultExternalIntentLauncher.kt b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/intent/DefaultExternalIntentLauncher.kt index a11317cb67..431bb49e57 100644 --- a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/intent/DefaultExternalIntentLauncher.kt +++ b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/intent/DefaultExternalIntentLauncher.kt @@ -9,14 +9,15 @@ package io.element.android.services.toolbox.impl.intent import android.content.Context import android.content.Intent -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultExternalIntentLauncher @Inject constructor( +@Inject +class DefaultExternalIntentLauncher( @ApplicationContext private val context: Context, ) : ExternalIntentLauncher { override fun launch(intent: Intent) { diff --git a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/sdk/DefaultBuildVersionSdkIntProvider.kt b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/sdk/DefaultBuildVersionSdkIntProvider.kt index ad57202d67..75904f4dea 100644 --- a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/sdk/DefaultBuildVersionSdkIntProvider.kt +++ b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/sdk/DefaultBuildVersionSdkIntProvider.kt @@ -8,13 +8,14 @@ package io.element.android.services.toolbox.impl.sdk import android.os.Build -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultBuildVersionSdkIntProvider @Inject constructor() : +@Inject +class DefaultBuildVersionSdkIntProvider : BuildVersionSdkIntProvider { override fun get() = Build.VERSION.SDK_INT } diff --git a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/strings/AndroidStringProvider.kt b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/strings/AndroidStringProvider.kt index 6ca82a0acd..1e07f77f20 100644 --- a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/strings/AndroidStringProvider.kt +++ b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/strings/AndroidStringProvider.kt @@ -10,13 +10,14 @@ package io.element.android.services.toolbox.impl.strings import android.content.res.Resources import androidx.annotation.PluralsRes import androidx.annotation.StringRes -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.services.toolbox.api.strings.StringProvider -import javax.inject.Inject @ContributesBinding(AppScope::class) -class AndroidStringProvider @Inject constructor(private val resources: Resources) : StringProvider { +@Inject +class AndroidStringProvider(private val resources: Resources) : StringProvider { override fun getString(@StringRes resId: Int): String { return resources.getString(resId) } diff --git a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/DefaultSystemClock.kt b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/DefaultSystemClock.kt index cbf926c00a..74c5094868 100644 --- a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/DefaultSystemClock.kt +++ b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/DefaultSystemClock.kt @@ -7,13 +7,14 @@ package io.element.android.services.toolbox.impl.systemclock -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.services.toolbox.api.systemclock.SystemClock -import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultSystemClock @Inject constructor() : SystemClock { +@Inject +class DefaultSystemClock : SystemClock { /** * Provides a UTC epoch in milliseconds * diff --git a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/TimeModule.kt b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/TimeModule.kt index 516985afb5..c1aa2c8be4 100644 --- a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/TimeModule.kt +++ b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/TimeModule.kt @@ -7,13 +7,13 @@ package io.element.android.services.toolbox.impl.systemclock -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides import kotlin.time.TimeSource -@Module +@BindingContainer @ContributesTo(AppScope::class) object TimeModule { @Provides diff --git a/settings.gradle.kts b/settings.gradle.kts index dd4bdd0ad4..9245034508 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,7 +9,7 @@ import java.net.URI pluginManagement { repositories { - includeBuild("plugins") + includeBuild("plugins") gradlePluginPortal() google() mavenCentral() @@ -18,7 +18,7 @@ pluginManagement { dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { - google() + google() mavenCentral() maven { url = URI("https://www.jitpack.io") @@ -44,8 +44,8 @@ include(":tests:detekt-rules") include(":tests:konsist") include(":tests:uitests") include(":tests:testutils") -include(":anvilannotations") -include(":anvilcodegen") +include(":annotations") +include(":codegen") fun includeProjects(directory: File, path: String, maxDepth: Int = 1) { directory.listFiles().orEmpty().also { it.sort() }.forEach { file -> diff --git a/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ByPreferencesDataStoreRule.kt b/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ByPreferencesDataStoreRule.kt new file mode 100644 index 0000000000..cb00dc18b1 --- /dev/null +++ b/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ByPreferencesDataStoreRule.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.detektrules + +import io.github.detekt.psi.fileName +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.KtPropertyDelegate + +class ByPreferencesDataStoreRule(config: Config) : Rule(config) { + override val issue: Issue = Issue( + id = "ByPreferencesDataStoreNotAllowed", + severity = Severity.Style, + description = "Avoid using `by preferencesDataStore(...)`, use `PreferenceDataStoreFactory.create(name)`instead.", + debt = Debt.FIVE_MINS, + ) + + override fun visitPropertyDelegate(delegate: KtPropertyDelegate) { + super.visitPropertyDelegate(delegate) + + if (delegate.containingKtFile.fileName == "DefaultPreferencesDataStoreFactory.kt") { + // Skip the rule for the DefaultPreferencesDataStoreFactory implementation + return + } + + if (delegate.text.startsWith("by preferencesDataStore")) { + report(CodeSmell( + issue = issue, + entity = Entity.from(delegate), + message = "Use `PreferenceDataStoreFactory.create(name)` instead of `by preferencesDataStore(...)`." + )) + } + } +} 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 index 8e0ed3ee50..9a7ba55dc6 100644 --- 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 @@ -18,6 +18,7 @@ class ElementRuleSetProvider : RuleSetProvider { id = ruleSetId, rules = listOf( RunCatchingRule(config), + ByPreferencesDataStoreRule(config), ) ) } 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 d24a489c6a..234a9c30ff 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 @@ -20,6 +20,7 @@ import com.lemonappdev.konsist.api.ext.list.withoutName import com.lemonappdev.konsist.api.ext.list.withoutNameStartingWith import com.lemonappdev.konsist.api.verify.assertEmpty import com.lemonappdev.konsist.api.verify.assertTrue +import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.Presenter import org.junit.Test @@ -44,6 +45,16 @@ class KonsistClassNameTest { } } + @Test + fun `Classes extending 'BaseFlowNode' should have 'FlowNode' suffix`() { + Konsist.scopeFromProject() + .classes() + .withAllParentsOf(BaseFlowNode::class) + .assertTrue { + it.name.endsWith("FlowNode") + } + } + @Test fun `Classes extending 'PreviewParameterProvider' name MUST end with 'Provider' and MUST contain provided class name`() { Konsist.scopeFromProduction() @@ -51,6 +62,7 @@ class KonsistClassNameTest { .withAllParentsOf(PreviewParameterProvider::class) .withoutName( "AspectRatioProvider", + "LoginModeViewErrorProvider", "OverlapRatioProvider", "TextFileContentProvider", ) diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistDiTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistDiTest.kt new file mode 100644 index 0000000000..093cfd4488 --- /dev/null +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistDiTest.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.tests.konsist + +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.ext.list.withAnnotationOf +import com.lemonappdev.konsist.api.ext.list.withParameter +import com.lemonappdev.konsist.api.verify.assertTrue +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.Inject +import org.junit.Test + +class KonsistDiTest { + @Test + fun `class annotated with @Inject should not have constructors with @Assisted parameter`() { + Konsist + .scopeFromProject() + .classes() + .withAnnotationOf(Inject::class) + .assertTrue( + additionalMessage = "Class with @Assisted parameter in constructor should be annotated with @AssistedInject and not @Inject" + ) { classDeclaration -> + classDeclaration.constructors + .withParameter { parameterDeclaration -> + parameterDeclaration.hasAnnotationOf(Assisted::class) + } + .isEmpty() + } + } +} 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 0fec5210d6..082a106be2 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 @@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.ext.list.withAllAnnotationsOf import com.lemonappdev.konsist.api.ext.list.withName +import com.lemonappdev.konsist.api.ext.list.withNameEndingWith import com.lemonappdev.konsist.api.ext.list.withoutName import com.lemonappdev.konsist.api.verify.assertEmpty import com.lemonappdev.konsist.api.verify.assertTrue @@ -32,6 +33,26 @@ class KonsistPreviewTest { } } + @Test + fun `Check functions with 'A11yPreview'`() { + Konsist + .scopeFromProject() + .functions() + .withNameEndingWith("A11yPreview") + .assertTrue( + additionalMessage = "Functions with 'A11yPreview' suffix should have '@Preview' annotation and not '@PreviewsDayNight'," + + " should contain 'ElementPreview' composable," + + " should contain the tested view" + + " and should be internal." + ) { + val testedView = it.name.removeSuffix("A11yPreview") + it.text.contains("$testedView(") && + it.hasAllAnnotationsOf(PreviewsDayNight::class).not() && + it.text.contains("ElementPreview") && + it.hasInternalModifier + } + } + @Test fun `Functions with '@PreviewsDayNight' annotation should contain 'ElementPreview' composable`() { Konsist @@ -61,6 +82,7 @@ class KonsistPreviewTest { "BackgroundVerticalGradientEnterprisePreview", "BackgroundVerticalGradientPreview", "ColorAliasesPreview", + "DefaultRoomListTopBarMultiAccountPreview", "DefaultRoomListTopBarWithIndicatorPreview", "FocusedEventEnterprisePreview", "FocusedEventPreview", @@ -93,11 +115,14 @@ class KonsistPreviewTest { "PollContentViewDisclosedPreview", "PollContentViewEndedPreview", "PollContentViewUndisclosedPreview", + "ProgressDialogWithContentPreview", + "ProgressDialogWithTextAndContentPreview", "ReadReceiptBottomSheetPreview", "RoomMemberListViewBannedPreview", "SasEmojisPreview", "SecureBackupSetupViewChangePreview", "SelectedUserCannotRemovePreview", + "SpaceMembersViewNoHeroesPreview", "TextComposerAddCaptionPreview", "TextComposerCaptionPreview", "TextComposerEditCaptionPreview", diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/Timber.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/Timber.kt new file mode 100644 index 0000000000..2aad499698 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/Timber.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.tests.testutils + +import timber.log.Timber + +fun plantTestTimber() { + Timber.plant(object : Timber.Tree() { + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + println("$tag: $message") + } + }) +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/TestParentNode.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/TestParentNode.kt new file mode 100644 index 0000000000..79def028f7 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/TestParentNode.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-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.node + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.EmptyNodeView +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.AssistedNodeFactory +import io.element.android.libraries.architecture.NodeFactoriesBindings +import io.element.android.libraries.di.DependencyInjectionGraphOwner +import kotlin.reflect.KClass + +/** + * A parent Node that can create a single type of child Node using the provided factory. + * This is useful to test a Feature entry point, by providing a fake parent that can create a + * child Node. + */ +class TestParentNode( + private val childNodeClass: KClass, + private val childNodeFactory: (buildContext: BuildContext, plugins: List) -> Child, +) : DependencyInjectionGraphOwner, + Node( + buildContext = BuildContext.Companion.root(savedStateMap = null), + plugins = emptyList(), + view = EmptyNodeView, + ) { + override val graph: NodeFactoriesBindings = NodeFactoriesBindings { + mapOf( + childNodeClass to AssistedNodeFactory { buildContext, plugins -> + childNodeFactory(buildContext, plugins) + } + ) + } + + companion object { + // Inline factory function with reified type parameter + inline fun create( + noinline childNodeFactory: (buildContext: BuildContext, plugins: List) -> Child, + ): TestParentNode { + return TestParentNode(Child::class, childNodeFactory) + } + } +} diff --git a/tests/uitests/src/test/kotlin/base/ComposablePreviewProvider.kt b/tests/uitests/src/test/kotlin/base/ComposablePreviewProvider.kt index c4dea95296..477e6a2da2 100644 --- a/tests/uitests/src/test/kotlin/base/ComposablePreviewProvider.kt +++ b/tests/uitests/src/test/kotlin/base/ComposablePreviewProvider.kt @@ -29,6 +29,7 @@ object ComposablePreviewProvider : TestParameter.TestParameterValuesProvider { AndroidComposablePreviewScanner() .scanPackageTrees(*PACKAGE_TREES) .getPreviews() + .filter { composablePreview -> composablePreview.methodName.endsWith("A11yPreview").not() } .withIndex() .toList() } @@ -36,6 +37,18 @@ object ComposablePreviewProvider : TestParameter.TestParameterValuesProvider { override fun provideValues(): List>> = values } +object ComposableA11yPreviewProvider : TestParameter.TestParameterValuesProvider { + private val values: List> by lazy { + AndroidComposablePreviewScanner() + .scanPackageTrees(*PACKAGE_TREES) + .getPreviews() + .filter { composablePreview -> composablePreview.methodName.endsWith("A11yPreview") } + .toList() + } + + override fun provideValues(): List> = values +} + object Shard1ComposablePreviewProvider : TestParameter.TestParameterValuesProvider { override fun provideValues(): List> = ComposablePreviewProvider.provideValues().filter { it.index % 4 == 0 }.map { it.value } diff --git a/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt b/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt index 02ffa418a3..114b2775f0 100644 --- a/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt +++ b/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.unit.Density import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi +import app.cash.paparazzi.RenderExtension import app.cash.paparazzi.TestName import com.android.resources.NightMode import io.element.android.compound.theme.ElementTheme @@ -112,7 +113,12 @@ fun createScreenshotIdFor(preview: ComposablePreview) = buil }.joinToString("_") object PaparazziPreviewRule { - fun createFor(preview: ComposablePreview, locale: String, deviceConfig: DeviceConfig = ScreenshotTest.defaultDeviceConfig): Paparazzi { + fun createFor( + preview: ComposablePreview, + locale: String, + deviceConfig: DeviceConfig = ScreenshotTest.defaultDeviceConfig, + renderExtensions: Set = setOf(), + ): Paparazzi { val densityScale = deviceConfig.density.dpiValue / 160f val customScreenHeight = preview.previewInfo.heightDp.takeIf { it >= 0 }?.let { it * densityScale }?.toInt() return Paparazzi( @@ -125,7 +131,8 @@ object PaparazziPreviewRule { softButtons = false, screenHeight = customScreenHeight ?: deviceConfig.screenHeight, ), - maxPercentDifference = 0.01 + maxPercentDifference = 0.01, + renderExtensions = renderExtensions, ) } } diff --git a/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt b/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt new file mode 100644 index 0000000000..d742db08b2 --- /dev/null +++ b/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.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 ui + +import app.cash.paparazzi.accessibility.AccessibilityRenderExtension +import base.ComposableA11yPreviewProvider +import base.PaparazziPreviewRule +import base.ScreenshotTest +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo +import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview + +/** + * Test that takes a preview and runs a screenshot test on it. + * It uses [ComposableA11yPreviewProvider] to test only previews that ends with "A11yPreview". + */ +@RunWith(TestParameterInjector::class) +class PreviewA11yTest( + @TestParameter(valuesProvider = ComposableA11yPreviewProvider::class) + val preview: ComposablePreview, +) { + @get:Rule + val paparazziRule = PaparazziPreviewRule.createFor( + preview = preview, + locale = "en", + renderExtensions = setOf(AccessibilityRenderExtension()), + ) + + @Test + fun snapshot() { + ScreenshotTest.runTest(paparazzi = paparazziRule, preview = preview, localeStr = "en") + } +} diff --git a/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Day_2_en.png index 3e22d6f5d9..6ed1ff9038 100644 --- a/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa7b667e43c0c85a2e4774549d1dbadd5c58049733cc51d9160ad5b245825cd9 -size 21844 +oid sha256:9d27f8fbaed8cdbb6ad03414cf6f8c2d38b3269731ee72f5fe9eab1ad6afe9df +size 21819 diff --git a/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Night_2_en.png index c52172b489..d213d6846f 100644 --- a/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:345f0bbff65c752f0bc3295f58befff79cf681d00b0a476e1ea5c9b625b81087 -size 19977 +oid sha256:4227570b201034be916a714dcf27c9ab4d17d6cc3d47a8eab9ffae3b9b61bb4a +size 19967 diff --git a/tests/uitests/src/test/snapshots/images/features.changeroommemberroles.impl_ChangeRolesView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.changeroommemberroles.impl_ChangeRolesView_Day_6_en.png index 4041cbc7ed..af622b1d7a 100644 --- a/tests/uitests/src/test/snapshots/images/features.changeroommemberroles.impl_ChangeRolesView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.changeroommemberroles.impl_ChangeRolesView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:623941550c734147c294ec2294840d9353ac756b054b402b365060602ff614b0 -size 58961 +oid sha256:3c08f5d276bdf222ec98929baa53275e40a76282c9bd15bbf3dce231223ea863 +size 59021 diff --git a/tests/uitests/src/test/snapshots/images/features.changeroommemberroles.impl_ChangeRolesView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.changeroommemberroles.impl_ChangeRolesView_Night_6_en.png index 55506783c0..91bcf2c395 100644 --- a/tests/uitests/src/test/snapshots/images/features.changeroommemberroles.impl_ChangeRolesView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.changeroommemberroles.impl_ChangeRolesView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17ca777cfd4e056078519282f928799dcb2e9aac1ab2757b7cb4cbb1fe93c573 -size 56855 +oid sha256:ded460697c44b23c47534b786d5f259d2ee87bbb6cffb8c6cbd6777bd40090c4 +size 56849 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 new file mode 100644 index 0000000000..338eb8d3f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7415b3286707130c0218a8d1e82a9d9c61c8eec8df346653dd67626d0dd7709c +size 10038 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 new file mode 100644 index 0000000000..5bd1590652 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39c67413ca997982b0fcd32831ccedee1d7a0763619784b8758b194df6c7ff81 +size 9639 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBarMultiAccount_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBarMultiAccount_Day_0_en.png new file mode 100644 index 0000000000..dd337cac3b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBarMultiAccount_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b289624c8461e08c945a254eb629ea536552e2652c7182be21a7bd9f183da022 +size 26185 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBarMultiAccount_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBarMultiAccount_Night_0_en.png new file mode 100644 index 0000000000..640aebb723 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBarMultiAccount_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b86aaf39367bd454a913c4ed6cd17bbaff52e54349e0e6d363d6f84ccdc43c5f +size 23678 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png new file mode 100644 index 0000000000..e574e4911e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc1c98131fdad16e22c63feb72536fee912ae84d763c57ff76eaee6d61889b4b +size 127697 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_1_en.png new file mode 100644 index 0000000000..60ab311cdc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a6d3be47ab7d9234657d4d088389c8aadb4d9e073d8c91f4f81dded1b6662a6 +size 42160 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png new file mode 100644 index 0000000000..41a0545535 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ed12d6dc439b7e7d86580ee24a5cd5c3b5e86b13c4f3cf3e64f677632df5872 +size 124816 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_1_en.png new file mode 100644 index 0000000000..2b50f7350b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3f70a15def31e67e84afe7f9545d280bdbca01c6dd63864662f19976df3bf33 +size 40960 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png new file mode 100644 index 0000000000..dc6b4dabbf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a666f932d1a595763b3c88b31ffbe3d195ec0d986ca5f2c0173e8e25a6115317 +size 126085 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png index 11ea72be9b..0252e8d778 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b819420fc0df9dfd4348dcd95bf46652422307b55bc58283a27130b810f57ce5 -size 25671 +oid sha256:cd023c99c75dba5987f86951d46e821d378440a35335dc46894ff127951ee50f +size 59135 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png index 87ec8be87d..e142b9451b 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f38f42eae4af268bed21fa69b9426708c7544f0ff558bb7bbeda721d5da29755 -size 22931 +oid sha256:6b9f321dfd59e2a388c0d864c73eca01c29b7f96ee0481597ee3cd8ff1df1781 +size 56262 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 index 76ec1f565b..c4e9be12cc 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24e40ff9b3ba80d0c00cd6b5a4e29b5b36f0e9ed92e4e6c123f49d88e661fe15 -size 18467 +oid sha256:44c81c89875c5159190cdb40c7d49849eabd8388d54db4bb1f6352b558e79ca0 +size 18626 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 index f7cf9ecd24..5474393374 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31fafcdd786b34313af790c5d9111e6f4787bc55587715357f7959b5554149bb -size 19772 +oid sha256:ebf4e51dee36793583638d31128421caee4166e86dd7007a6342729c28e7058c +size 19945 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 index 76ec1f565b..c4e9be12cc 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24e40ff9b3ba80d0c00cd6b5a4e29b5b36f0e9ed92e4e6c123f49d88e661fe15 -size 18467 +oid sha256:44c81c89875c5159190cdb40c7d49849eabd8388d54db4bb1f6352b558e79ca0 +size 18626 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 index 5d7460384c..bab506a7de 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7346cb947c370f125c23604c39c499a4300334e12d34a8777a9538a2eea2e72d -size 17157 +oid sha256:5825ff69bffd527601323f52f2ba033cb7b9d754b5113d851dd68a0e5c09e45a +size 17308 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 index 698b15df00..d0fd29ba04 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81070899b4d7fa3f0620e3d135d85c09746cefa80189ce9a9c8115311c6dd0e3 -size 18293 +oid sha256:06ce5a60d11da9217ba3f70f794df6d51e1c319052bc063f26466c9b9c31e838 +size 18448 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 index 5d7460384c..bab506a7de 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7346cb947c370f125c23604c39c499a4300334e12d34a8777a9538a2eea2e72d -size 17157 +oid sha256:5825ff69bffd527601323f52f2ba033cb7b9d754b5113d851dd68a0e5c09e45a +size 17308 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_9_en.png new file mode 100644 index 0000000000..1e3446160f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94341fe8aa52b4551ef8a73cf298aaa1c4352ae0d4c83990c04c4c6e491b64e8 +size 21444 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_9_en.png new file mode 100644 index 0000000000..2de946c312 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00a961c76598dc98a0d9c7a45781865984c51bb440bb1605c381e9496ca1789f +size 21971 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 41c9460979..fc843bc4f5 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:7423c3f11ec8661d7fb3f57d7d78459e784221644229515080e0b0dc6ce59b6e -size 10129 +oid sha256:8c88f9a3ecbcf7db13846a6074c75fca2ce6bdbe0edd850bfc139e01a4fdc0c8 +size 9956 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 bb3bc2a09c..4f172d6a06 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:08ed98b594a6730d52ab344aff6bea8a38216a609549be267f9046dd69858903 -size 38478 +oid sha256:7fdd74d6df903a0cedfbfc2f30430b5802e4f0bc7b2711e20ae47d297066b056 +size 40297 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 97af44e438..ce1e584d59 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:bf0a1c9fea2a067d6156985cce2fd2a2551f6111e7739206f7bd70a563a97fcf -size 45333 +oid sha256:ed26457abf2a6d89d61e911944b4e9f62de248c1939f997d0f8764e4c1ae08e9 +size 43088 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 4176533a78..ad220df759 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:24d9f9b2a63773f5bc49b7f8f8b85d1dd13e8e62175b61bca534b04626d19b0f -size 46336 +oid sha256:46b410cba77b24d8657031f5fde29d4f25a639300862641ce0f8ad0de8ff99cb +size 44076 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 index c737e362de..af3739d987 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca5217bba4fe65af4ac2a0aaaefa941a5a5ed0735dbd9bd69a648a157ca0c51b -size 29343 +oid sha256:950e90b7c2c67fc95c9d6698dc7436885ee80dcf9da7d8467b0027c04c03c8f5 +size 29243 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 index f3ad94a6a5..0834071308 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41a3c9cbec371b6aacabcb954315a65939b579c5963db63fc11789326ad2fc1b -size 29068 +oid sha256:37ac3a032e758c4abf9ac8a87e887c7e015f2d35160d59a7e263963300bd0941 +size 31558 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 index 70d7d9d00e..330bd7edc7 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0249fb4df9e88abaa28cc3a10e2cadc23b3b84581d74893d525076683f0a5588 -size 36386 +oid sha256:44b9c4608696c6a690e7f00f661b3cb55d03826b11569b6e8ce0a48f9754dd39 +size 38547 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 index 52411087a2..e9ce885cd8 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1c6c00bca10e0bf725ab5debe3cd9dbb14be89f29872ca70df8f3886d1cec19 -size 40928 +oid sha256:f92065e3de67f39ce27ae3e321f59fa2d18b5927752dc760450ec275935b1959 +size 43405 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 66639e1bc2..c127d5b02f 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:96ecb81ebfc7545eca7931538a443b7190b7903d8ebf5bf70bb95eb3b42e6a38 -size 30092 +oid sha256:919101bfd215af97a098bdfae4980445bd1d0560f96fe9ce13a870e2d8817fff +size 34247 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 b28d9d527d..a902aef055 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:09e79f148754898125af4972bbfecfb1e3fc8f2de918ba9dd12d9766ef09e4f1 -size 31417 +oid sha256:2f9707b754041c871c408e12613faa186fa4f4f760ca793caf20c62bae07246b +size 32021 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 461aaa3497..16ac399dbf 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:6c87682ca77c0ee163e437dae9f177e52937c6099e1d3583f2138735fd5ecc8f -size 26669 +oid sha256:5ad9105ca2659f51b16b41f9317812590f38adb8431a64a139bb47f54fdf9eff +size 29754 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 488b2ef5d5..78b8500823 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:8319fdce8ae47fd6dc7aea8f6c98e9e4de7322f0b9adc4262695ff3990a63052 -size 39520 +oid sha256:04180611d2a959d07bbbd051c4386f6428d841c5ea74e197a80149370db76e04 +size 41830 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 64d8d932d7..644110ef6d 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:c444dbd6815dcb6362b3a595c9a052e81629c09f90cae08cb42f2053ee06d129 -size 27689 +oid sha256:7c4a828b9aa4e4963e3cb39e6f65f82ff5e595fdccb4dc5b48db532eeac1fb00 +size 31388 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 ce70cfc15f..c7d05de034 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:484657673c4ae676b330320ba213485a16c6f31ea025b16b4f2c3a53053eaf7a -size 29860 +oid sha256:bdc24799de625f9826fcf87a624e76b530e7f0e44c294d49f842ed2de7a9c085 +size 32981 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 1484121044..3d78e94953 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:a239f4f8003693097fee6e9dddc31ed1ce7a53f7c2b9dab9db55059ca51df64f -size 37645 +oid sha256:247a1cec9f0729ef1b23dc5ad730521bb7bfc117b4e9cc901ec922ee946f942e +size 39401 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 979b3141be..c6f8b658a3 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:8e23c1347af06b2fa6f1982159cdb92b2bed278ab1b4caf782c3916520681d75 -size 26616 +oid sha256:bcc00accf1fa84c48831378faa27558d5ba24961dd3ba0c5f0587329cdbaa9bf +size 29110 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 74638ec5ac..cc08744b50 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:657dfa3cd260803b3386ae6c3d44f6cc4277c2ef2572680145c419dfd9690e7a -size 33276 +oid sha256:da07e3041ec5814feb0d14e8478d0c817d1496155db2daa612048b94530df20b +size 38200 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 37396ce4d7..636b7ba0e6 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:58776319728d3367a347d62cf120803582b05c564f9cd76b2c8f406e70673821 -size 9949 +oid sha256:c91724171606aa0c36987908af0abce5c05b3311469a08e532532307d1224c60 +size 9842 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 7d9743ada7..edc2a732b6 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:4ae699d09bc1287d12d9974a6801f6b1648f0d6bd63eb9ac10302be37cae5383 -size 38181 +oid sha256:f5a6da57cc5659ba3f3705cba41fd23c7628ba2477fcfc427b8e26bde37ed812 +size 39960 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 6cdda44336..5f8961f485 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:511985d79fe13467eec62772dbe6cc69dec1f1c9f9a3e66428fe9b22ce279d1f -size 44937 +oid sha256:5b9cf46f0039e0f91e8372cd5f9a15b7b54b75c80e0a38f9ebc2548315d57c9f +size 42657 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 a1fd91d0a7..c25392b215 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:a084ed7657a2f263283a477e700df9fb6219e8268380f764c2e5e4055465044d -size 45898 +oid sha256:5a4f65342c0e84179aceadc5d75fc763ed45c23f4db44bc16441c50990b193ed +size 43582 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 index f2c3b773b4..9c134bf310 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51680be3eede0c4a53683b298415a07f3e9ea688ac39fe29373e1e80b0f4540b -size 28778 +oid sha256:e2ef8069be91c6e20263da790c0b8b777515f46bdc4769532b321ae86c9152b8 +size 28701 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 index 485ba35c62..867ef2ea72 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7172438656b08d5c244fc570fd25dc074562b7599f15eda8241ddb7c9b275fcd -size 28765 +oid sha256:5f47c18fda5c4839e1098955f57c4a3e5b9856203c6a84c31903aa5e0b2da46b +size 30971 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 index 4ab802917f..40257d2267 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9aaf1721d3b8c3a000402eb605b7ba742560fae7297514a15f74fd97c2f838a1 -size 35406 +oid sha256:112286f6a18865db2333a68cb804e930eb873bb04596bada2f825e2223b4f537 +size 37362 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 index 40682697ef..abc81a0d58 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7b113b174727ce08ed2b23f320d49b556e869249302c0b84c1b5916ae8864ac -size 40563 +oid sha256:ff86f906357d0bf611bfc9cd235e66e0247dd7bb66903cb0e5730224d5dc897c +size 43008 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 251f240e62..b480466344 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:444f717f986b154297294df6ba731befc3e2049f89a3294e26fd6d90f0831996 -size 29353 +oid sha256:cc849f3158de5cd3cdc64a57e613f3b3a5cd70794e7984d7cfa1f8f425184592 +size 33440 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 2126595abd..9869f158e6 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:f9bd6d13f3c4f7a715e9e8698db252bc33823fe180e6d279eabcc9de618dcaee -size 31182 +oid sha256:ed211c9bc258185de6ea4cc414fd214f256ad1649c26a8a28f25fd1b29c34783 +size 31630 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 be61677a61..42619d6321 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:acdd2cd73f2a37190bbbeb829af68a28dae8d73f5462396db4c6238ee8319351 -size 26733 +oid sha256:6a88ffdfd4259a2e85b3a8375591eab3b0f12e927eb83f9c6820995acf44d79e +size 29607 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 c484f2aeff..1b95755ca6 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:731f81d6ca0d4677dc56b58316c7396a7ad1ee08c785aecbb9eea4a0dea6b77a -size 38428 +oid sha256:7fbe20a3d4e0c3706ce81c1f52c65ff4f749b0afd6bdd3efb4100ee1019afc51 +size 40580 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 7c8b6e4d8a..0211928122 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:4889f91529fddf07ddccb9d1b31f4af4278245d9916694d2197798318fb9469b -size 26163 +oid sha256:cff1f0a8a34cfd4eb55487540a02c70e629d7f6123355c95380498817053eb78 +size 29575 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 76d730668b..f599f084b6 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:e0d60be4104d5f124e49e8a7434204bd0593b46fca838e787aefa40769ac1778 -size 29202 +oid sha256:71e56b906decbea31fcf5581dc92dcbb6be79f6a6962595786e9533cc62254ad +size 32045 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 799003fe8e..4f8d2856a4 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:7129f65572d7b4f6b302fa803f878b269f2ee7b11a684c3e1ec051f9192fc8fa -size 36927 +oid sha256:18e36242d664e1f2179bb3aef3f77a5bbbf5cd44aece2d2c9a3d7d70d2356b92 +size 38769 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 7c9d79d83e..b595df5d84 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:809913705eb237bdd8575a92be89cf616d27c7d54a32894378f693dbf1969d0a -size 24731 +oid sha256:37270eed539ed52a113375dbfa516f443838530ef7995bb6372764afbc25e75d +size 26787 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 5098ca7b17..8af0e7e350 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:8c862dd75be71248752d53cc605860c55fe4eecce75eba545ab4471a13585296 -size 32951 +oid sha256:0b73e8a2429db6324f471908f46ccd2f6723d59caf4acc738c07d6e402a19285 +size 37752 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_5_en.png index aa391725b3..22f56c4da8 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c06f9cadc81daeee0925d8684ad70f7f1bf2dac56c30dd4067c223683e5cbde -size 45382 +oid sha256:c6e13b1e5c43efab5cc40daa0118566aea330552075e5409564c496b86db4564 +size 45402 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_5_en.png index b7b132a64c..4312dcda4b 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b4f3e43f6f01dcf53f27aca683733015781a1b46b9f2db2fa8f3500f5ce802c -size 43246 +oid sha256:d4ae8ce188cf5d380117fef6f98c73285486970267db88b0823409e75fdf1c1b +size 43250 diff --git a/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Day_5_en.png index aa695c8d55..c0d747d0ad 100644 --- a/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e409ac134f70890d14c31fe0939eb73f1aa010915981b344d9466e9c2450b94b -size 31954 +oid sha256:3f5e1530d78162b77a2d8c79142887f28677aba2acaef31c684231202974b3ad +size 31890 diff --git a/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Day_7_en.png index 5a1e48d6a2..f5483d0828 100644 --- a/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a6a89b15219084b3daeec9cbac6304ece498156ead77a003d8043a85660bf2a -size 19286 +oid sha256:ea9f384fe887976aafe2c3f37f077bce21d2234408992a5ea9865594e405a8b8 +size 19451 diff --git a/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Night_5_en.png index e843f51690..ac82f69973 100644 --- a/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a416901c2228c2aee4f8a925bdb87a64dc36ed7727e8897c04205cc4d4147840 -size 30096 +oid sha256:2ec0da2e0489ae016f1cde361aa556ea7b74de12dfa41134fb8306521dd6d14a +size 30137 diff --git a/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Night_7_en.png index 27afcc195a..a90b8327e5 100644 --- a/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.leaveroom.impl_LeaveRoomView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5e028a3770cf87ebdd9d44c012bbad6c409b7edfaccf5607ab5d7a6e6c3194c -size 17713 +oid sha256:40737bdbc9d264e7e1ea43c7c79463f88de844f518f45e0e4c856d78a0e9d6c7 +size 17868 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en.png index 3cc5e9735e..defcb90471 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3d870d023b29f46e356184fc45a2dc6010a088b1b32a690d0d98b43c7bd3eb2 -size 28845 +oid sha256:aa15f390927a533cdff4d42751e19942b0386473301cdb8cd800b37428ca2179 +size 28851 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en.png index ddae20fe60..c2dbf39b83 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e636767998635b6abb3dbe29af0d42346c7b249125865f5b5a559abffb28ce5 -size 26963 +oid sha256:91251a1871095a734f70d335b3f514d266a5486b1fc57cbd130214f268525f1f +size 26957 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 035cebd446..085c34f992 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:ed0200ed72e0d5551e28c90eea2ea3cef40cbcd5fc738839700d42373322ac4b -size 25139 +oid sha256:dbf3b090b0e3b4a00f5ddd86b7b2a62baa62e9cb6d1c24c36191620124dd6d12 +size 25210 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 d66517b825..a50a3c140a 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:6411ff6829376d545081c2b66db42e7ce8bafbf72d2809d884f81a56864e0aab -size 29732 +oid sha256:53187c4901a528f5ea59fff3def64197eb0664ebffe85f48635c5cf1720c977a +size 29700 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 7b92e39b10..78f23cecd5 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:8f5fb7a9c99700b05fe90af1ae17428202e4a2d4c0e5c1f246825160e3a42b15 -size 23148 +oid sha256:5a430db03a543cf4517f9086e3bdf43cae72ac544f5fa1b5994bf62aa3151463 +size 23228 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 727473d6a6..ca9ce8c967 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:6e90c76587699c68a30a788ed8fdacfa7a53f49e7bfeea0c7a2809d5b4b5e662 -size 27622 +oid sha256:0870a37c29bd5b62b559e47c0eb2dfd7ae94ed38efd62d3fa91eb0d646923373 +size 27565 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_4_en.png index dcfebea1ec..4b0e30affb 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41280716b1e864b904e073171ab0f3aaec6fdc8917481259924db581196fd1ed -size 26115 +oid sha256:50002e3a2f804fd2be0953fdb751ca73582a542346e0b854e256264e4f3ab77f +size 26047 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_4_en.png index 8984aa75d5..a0af69b135 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d52037a99c6094a488c3fe097a83a01b9de75c43ac3ead61f0c1e451783bbcf7 -size 24912 +oid sha256:48469fbca8f0b6c4c1fae05db27c1117ce6284bd6167a5f27dc06dd7d3c02a19 +size 24862 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Day_2_en.png index 8f1e86273e..e448af41a9 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65bffac39daeea85b38ef1379cb3ec35bbe5bf0a230dd5b66f342a58c12d6e0a -size 25701 +oid sha256:bc0fb8588ecf00b34d52da98594ee1aa841eef98cf5825fbea535e1fa871a2ef +size 25646 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Day_5_en.png new file mode 100644 index 0000000000..2e7635cfdb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4eaf3d650155e9a779cf796cef116eaba0f1d6722b229332b224475393a88178 +size 16828 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Night_2_en.png index 0a32ebc655..4aa9046ed9 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20263696842e1790bae3bcbceb6268f9d960de48e1a32b2459ab5abeaf5d20d6 -size 24506 +oid sha256:0c60a197c679eda6323efacad6a33acc6f0175ebd0af7928609313c2189d6b9a +size 24455 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Night_5_en.png new file mode 100644 index 0000000000..10ca16827e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59f062f54833df71be9d7c4e785bb01013a10642e0d863bf7ef2abd8862b93c8 +size 15476 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_6_en.png new file mode 100644 index 0000000000..7a0469e0a4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06af9be7c045759dbe9379b02996f3cbab7d7e52315080c1d61347a88a6b1957 +size 281422 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_7_en.png new file mode 100644 index 0000000000..cdd0ab890c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0c1cb36f30969765191d54131c862a06ec749668f79e5cf025f487ddda1ca0c +size 21891 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_6_en.png new file mode 100644 index 0000000000..8423d6b535 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5aa2a7263ae623143dfb687d9b92b3ff44d2388ee613c523f9533ae596902064 +size 280537 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_7_en.png new file mode 100644 index 0000000000..f45558ed75 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f25dd05a8be436b2a6d9721080dc460f50bc7a0549885144a285a428a80a2e3f +size 20982 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 8dfb060ca6..0314ca1c8f 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:e2ecb0a9f48a2c43c700dab7b21497d5bebdb63ad87a63d15525f596374b6748 -size 61650 +oid sha256:d81131943b6ff08a8c52827e956a9cf8d830b8523c192e645b116733aff2f8e5 +size 61663 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 ba552634c9..7b590a03ac 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:cbb1c1dc96055d4fe6aa792363abc00fb72bd8af758b9e8597f45725a983111d -size 57951 +oid sha256:5204b189cf02f8f0145427df1c412e597cea106bcb87ac259b28e41ba55f2f0c +size 58053 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 0e59b57f97..d7254dca40 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:e85eb3d968eca031af366d557a1afff7aa6f427f9c5761c4568b807562248fb9 -size 48880 +oid sha256:cefbe57afc5b78b598d004fa9f6cc9d42e00127ffb7900a2f5346eeee0b1531c +size 49183 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 743bb72999..789df60a7c 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:29276b02a32bfb0eacc9fc4853711f0d43b4e9e4dc927d7066ed0d0b1ee12680 -size 50357 +oid sha256:03fbd054c197e783d2079cf4b3953f1a4f15dc7c38889dc826cb5de3b7c1f4d0 +size 50597 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 c4ce6ff884..4568d23092 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:494e17d6cb91effd9351b9e331ffb4d8c4da927b28bfa8e740fdb7d865bc7fe3 -size 43609 +oid sha256:34c5fd10902041682c6ab2c3022ea44977035f7de6ea9844d94876b921327ced +size 43960 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 d9507346e2..dca9066157 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:3ec6dd722d802baa463784587d4d8844edb9a38e623fffe2fc347970e437cac6 -size 46583 +oid sha256:531e376ef91fdb4f535a951222f5e25ecacc7ded6d6693e30551ec68a16dcd33 +size 46911 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 adadb27faf..c051c9bd74 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:204de297913cef7d13569bb8b51135c6a296ea6eec058dc1ce31ebd547137887 -size 44947 +oid sha256:c18f38af96a7f2d0e6dd30827d9ef5cae25d7c864df81ba763044f782f3bb7c7 +size 45233 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 7aa0b405ed..13a4439f26 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:66bb9985aee2e4988f3beaf58e7979b0812876570d5905a31406069546aaf0e9 -size 42010 +oid sha256:96472d116b4c961b4dc44e7a5e8fcf9a7751ed7662e8c4a169722ff53a53ceed +size 42380 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 5437089abe..10ee8e348d 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:7729240a99b1fbf3caba5c480806f88f441d4c17f796808d6f50979ae76ecbe9 -size 45125 +oid sha256:055df03c64585976d1453c0c3b5b0460cefaf1aaa93a47d17b98cc80fba11f29 +size 45525 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 90b6b9c5d8..05e6b2c7b7 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:0f6bec6647ac0678ef3420e222ea2373c4d618f7eee32347548c0fe35a7620b2 -size 43261 +oid sha256:21a5e088364ec86448df788e0d54c27dcba3c6f696a86e7f97289da0792ede75 +size 43598 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 91c0d21b39..36ecb96acc 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:6a97331fddcbc4c89743e15bd79cc6e753b59be0f33f41de2d8cc184b3e1f3ec -size 45084 +oid sha256:e356e49f62609b0c96b294a51af0c3dba861906d0cf18492460b2d199dcd1003 +size 45368 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 6fdacd6bb3..e762a68e92 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:48ca8baddefc71d98aa4060d730f7785e654ed078f5c9aa21459df5b730b3acc -size 47969 +oid sha256:a883e703a5f8c9a794ca274e52b493c490815f39c84c7c7b9c9cef348ecfb766 +size 48368 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 bb894571fb..67b93b5e8b 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:c0d19fe576a9cb4895ccae8c3f862239220b9542aa0c4c102cf5a99a0184ad94 -size 49392 +oid sha256:c44806a243336438401a21e265c8d513fff7e4f2cbf68a0c745f5918c89d639a +size 49828 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 15896e77fc..ff27990665 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:d81fdfb3340f8d8d63a0529eb2f4d035ebd4789aed0c98272bdbcb9c4340c7c9 -size 42544 +oid sha256:9725a74d334baddaf3946675b871abcc24f20e3518fe572f6eaffdcd5e0bf98e +size 43028 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 08b5933d26..ddf220b94f 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:e882786b51e7692f52f1938cf1a29581ec299e4bcb7a3253ba69009140a29233 -size 45837 +oid sha256:78379d56dda45b77f6bcfbf32e12e78f72e88c760d7a0911bb9e8c9f244c39b8 +size 46151 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 d66e2f116b..39a4ed6170 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:fbf6bda60c2f68bfb41405919d669b7aec8db408d49593ab0520b5b9afec8627 -size 44217 +oid sha256:b8213b5f1c9620cc8edaaa9971906f09c8b466d42ce253a9f52d30419af2e732 +size 44722 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 54af19efc9..05aa90f6c5 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:d68f839ba6d6aa3d9bd9fde6d71a68d95130011aebaf43c87eaf8ff826f48a4d -size 40689 +oid sha256:5c94f65df581c57f83346e9c8d697a50eb95cffa5ac3fc4198017a41b9d72537 +size 41215 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 840cf053e8..6863297844 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:3c139a1a328cef4752219fd11df4c79b19c47501df26278811cb7775269988c4 -size 44501 +oid sha256:9fc2b4034704979785701cb42fe2b967256a6151ac90934cecd5b8b1922f780b +size 44987 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 9140f6932e..cedfe0a891 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:f17461d5b74d2c1c493e58236793978a5e96bf5bf15b2ab0179dd0b39b9e626a -size 41948 +oid sha256:d14b2d18fb60fcabb3e657001c97d95f55b82e87cf8efa34c8a5fb2ab237a359 +size 42385 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 9c6ca432df..ed3e934f07 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:f5a920e128caf9d5609d9030e5d37cf951011bd51009c988d84873c993fff95d -size 44331 +oid sha256:5d081025bedebd0993968f3522a6e4266ecd20526284c37794a7180305d07c7c +size 44806 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_6_en.png index 8cb89ad06b..41a7341afa 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9368b86a0b2c5f73e97837019327e2f3c7898ded80de259a7c03e58f2ccf2b64 -size 72852 +oid sha256:a4214287ce64b1083c0be5227fd6e16066f3932c48ce7a8953d47999a5924fbf +size 72848 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 8d0c6973de..345ce9fe34 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:b4559bcfab614255a51b2f73869d5ffc91fbc9fa53ef61c2ae02bd28c88bd838 -size 20773 +oid sha256:1d194ea0d68d15d8950277eb747047b7ac3c809eecfc12877296b7eb67d67b02 +size 20707 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 760ee954e9..25a06027ed 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:29c5b7ad11fdc97564ce4be236f3b89785029ed65d75be23ba97f5267438739d -size 18932 +oid sha256:4dc9805c2f2d910491da944f1cf91430bcc5a8a91137eaaf301f65955d0d350e +size 18798 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en.png new file mode 100644 index 0000000000..04c099106f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65a59efc8cfd19f4dddf0ed9d39c4cab634ebc720c2da7737f382dd2e325ad37 +size 22517 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en.png new file mode 100644 index 0000000000..045c8dba15 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b7fdc137bb92ef23da15f9caad89a095355c719a4151b10d3f6156d04ab7055 +size 6807 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_2_en.png new file mode 100644 index 0000000000..3e93b95261 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9ea09c61e22ba6bf59a93f06f89ae738d4cde1199fe79d2b0aed86805ddabab +size 5636 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_3_en.png new file mode 100644 index 0000000000..cc8c7de1ba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da07b6a6c81f2d819247b77cb6452bf808a6ba9097143e8ca6acf85025ab5b3d +size 14438 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en.png new file mode 100644 index 0000000000..42c3854905 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18b5a57b061028e9b0d265f725e5c2b08439589c778c49f054822ab3baab68bb +size 21551 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en.png new file mode 100644 index 0000000000..493c2947a1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12be2dd55a8f06cb5fe3ffcd0364a0cfee77a4ea502a31044c2c201ace026e49 +size 6702 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_2_en.png new file mode 100644 index 0000000000..136b09b57a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ffe383d6b566d8ea19d335012e8aeff51de67fb75b208532a3c07bc02d76464 +size 5523 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_3_en.png new file mode 100644 index 0000000000..cc96f69e93 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8b0196422fbee4892e4909c027de98ca9f98c1d4d9c5d09fe8606b120f0e4de +size 14264 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 deleted file mode 100644 index aaef7bf555..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5bbf091e13abc9a1d23fb01419d2fd17cf8c2e9e87cdd25d45e52d3c97ae04f4 -size 231965 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 deleted file mode 100644 index 46be57e49b..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cf9a76ede9a298829f4df8f1b6d5abcc25d841dd16be2d9331b1c4da8e84e8f6 -size 234898 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_ThreadSummaryView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_ThreadSummaryView_Day_0_en.png new file mode 100644 index 0000000000..32733b09b6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_ThreadSummaryView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2cb3989055cf63e3ee14d8e2d8b2cdcd67b3af1addae78b38ab13ddcf93a232d +size 9740 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_ThreadSummaryView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_ThreadSummaryView_Night_0_en.png new file mode 100644 index 0000000000..9b9901528b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_ThreadSummaryView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e206a2485736baa2ac8c6dae6847ef381bfa1f36a5b7bda14a7f93a55069f41 +size 9666 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_en.png index 4f8761e4dd..5695ab7dc3 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ddaa247eea635d7e8c079ae128983fd25c1aeba96a48b08ae71218ef52e88d5b -size 69475 +oid sha256:b51cd7375e0d9be97786632eb1dfc16b5023784ed0de616f029d782ca56aa07b +size 68221 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Night_0_en.png index f17dbbe01e..a9b8b7cfc5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:647365688fd0c542d4be6dae570b33fbb7bab6261dc57791db85966808d440bb -size 67563 +oid sha256:1cb184afd2b1afff7b2f5f24f555e8278e20bbc98fe8b67a25a005771c7105b0 +size 66908 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Day_0_en.png new file mode 100644 index 0000000000..571661e434 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:065466dc414d77a58f2d22c26fc9d5e9b7afe5d5206cdbcc2bbdc6e8ea192d15 +size 40479 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png new file mode 100644 index 0000000000..958d12766d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c34b8c47d89231b8e5660726d63b94e3e8b97a2fc7e0b615f8f9e2d61066470 +size 39172 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_ThreadTopBar_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_ThreadTopBar_Day_0_en.png new file mode 100644 index 0000000000..079ac780f8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_ThreadTopBar_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:617a819de3cd223323b0fa1853185ceb26ffb97a22d79524acec0f4d5b3a634b +size 33550 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_ThreadTopBar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_ThreadTopBar_Night_0_en.png new file mode 100644 index 0000000000..b3f0e9c13e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_ThreadTopBar_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e146eee5e94e29a2b87b3d4a9bf248445e08dc6eb68c2268b6f263533675c78 +size 33050 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 94548f50d8..11b3313094 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:e1ffac0c95378fa7778a4d5b3e5af5a47eef44757f13dbe0032d30b30f96561b -size 58549 +oid sha256:9571176fb73b9871b6db3c30679a46c776badb56d4e1919ea882b391d893b8c4 +size 53144 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png deleted file mode 100644 index 4ab2179c6b..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:983db5846ee050f682b895ef0ce54362b1214c5bee48ef94139ef825e0ecbab6 -size 61678 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 deleted file mode 100644 index 788ff1abe6..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_12_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:501b8bc5c6163c85d1aaee4c9b81c56610931b6d693421d87b5107b3389170ea -size 60829 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 deleted file mode 100644 index 25a8d03e4a..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_13_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b116183b02d89b112d35ae86d68fdfcec0cb63e5ded20c611a152836090b19b2 -size 60755 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 deleted file mode 100644 index 22fda1dd27..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_14_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0fd783aa6c219e5bdc3c03579bd823a286e6048581728a6786ee724ef458050c -size 63807 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_15_en.png deleted file mode 100644 index ff317ac8ad..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_15_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:045e0d9605be905e4bbc5d88fc5cfa3bd4a109164709ce5276dedc3dbcf2da80 -size 51119 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 abd6861f7e..657dc6d2ac 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:373b60146f1fe215eea0c559fbe9810bfe9b714f021e3dddbb649dd6105c5f43 -size 59547 +oid sha256:f6a08f9b69ec278f8706c8de459fd6d2747cef7ed51da32eb7239667d0805e58 +size 55311 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 657dc6d2ac..3d2d2949ab 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:f6a08f9b69ec278f8706c8de459fd6d2747cef7ed51da32eb7239667d0805e58 -size 55311 +oid sha256:1b298a6432190f54fdb824c7edc2f8cda30a746d2a13322793c84a5a3d42f5ea +size 59985 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 3d2d2949ab..e3b8e08ac7 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:1b298a6432190f54fdb824c7edc2f8cda30a746d2a13322793c84a5a3d42f5ea -size 59985 +oid sha256:61add48c03d1f4df4bc9010826c4a6f5adaa312ef5f910bc0b3a77211a377f66 +size 50651 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 8c8344673d..4ab2179c6b 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:34a70e62b687f5726073a2339a4246f0e6bf31c187483829082612be69938087 -size 60107 +oid sha256:983db5846ee050f682b895ef0ce54362b1214c5bee48ef94139ef825e0ecbab6 +size 61678 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 e3b8e08ac7..22fda1dd27 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:61add48c03d1f4df4bc9010826c4a6f5adaa312ef5f910bc0b3a77211a377f66 -size 50651 +oid sha256:0fd783aa6c219e5bdc3c03579bd823a286e6048581728a6786ee724ef458050c +size 63807 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 68918896b2..7d33111f68 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:edc830108c2dd54d594a383020c2e73dba6b6fdff44e279b87889f3dc84ea546 -size 56308 +oid sha256:d6ea6d7eb98c546dbee109d160e2adbbbd4d22d4844835667bfbab1de2060115 +size 52351 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png deleted file mode 100644 index 0ef6da37c9..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bf12305ee99f199905e94e325743b1589f11d36815e16e86558691400e2991e3 -size 59022 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 deleted file mode 100644 index 98512acaa5..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_12_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e1b50937116f9d14103a1753199686f959371b8fe6ad006cadf1c3fb121fb72a -size 58491 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 deleted file mode 100644 index 9ed7763072..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_13_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8f5c6283d080341dd600f723329355447fa190c8b36aa4241536c4993ce55b76 -size 58466 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 deleted file mode 100644 index 998c5ab138..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_14_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:01ea3893df495012f2e417e81d2223696ee11efe2499c0c2a112f015c1f72f7a -size 64610 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_15_en.png deleted file mode 100644 index 7fb660b0fb..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_15_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fdb2ca59f48d38d8fa7ebeb9d5768b44f216266a27f6cec9d518b729ecece253 -size 50253 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 06f314231c..c041fd7d11 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:21ef350495865cd55510b9dd59ffcb7fa3600369df3077322c352e47cc8d93b1 -size 57291 +oid sha256:c1534b9c3b6952c290ca20a2853e56be023e5c8129a10b9f3fd63c290c7ba9fb +size 52977 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 c041fd7d11..7a6efabb58 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:c1534b9c3b6952c290ca20a2853e56be023e5c8129a10b9f3fd63c290c7ba9fb -size 52977 +oid sha256:7b50e874595cfcb65cf2a4f7146ed7c99ee06899d6512024b900a64439748a6f +size 54078 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 7a6efabb58..21888106d2 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:7b50e874595cfcb65cf2a4f7146ed7c99ee06899d6512024b900a64439748a6f -size 54078 +oid sha256:aafdc477f090674d0473076071049bba77bc57025c602cb6bc1cdfada03a1937 +size 44740 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 2745c0ff60..0ef6da37c9 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:b5052598f1d095bb854522a2b0e880a6a88c070e372e1a2fd73b487611f8f733 -size 57716 +oid sha256:bf12305ee99f199905e94e325743b1589f11d36815e16e86558691400e2991e3 +size 59022 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 21888106d2..998c5ab138 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:aafdc477f090674d0473076071049bba77bc57025c602cb6bc1cdfada03a1937 -size 44740 +oid sha256:01ea3893df495012f2e417e81d2223696ee11efe2499c0c2a112f015c1f72f7a +size 64610 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_6_en.png index 43bcd29a9b..d79f995fa3 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3028295864a6990944ec3485d8b7c1aa1688005f34b7519e66176c6d945aff2c -size 39907 +oid sha256:a049311411b2e8bb777d2fefc74c06e943c36931e24419c1b9155472643a6f90 +size 39868 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 4ea3d6f1fe..ab1af1559f 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:d3ae8baf10cc1adb8bb3dbf4bfcd2653876dd9c902b07d33bd4f5690d18ec263 -size 44378 +oid sha256:f6a577b1e7047237c485fbcf49d238478f638e471e5f637721c7766eacc89799 +size 44275 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_6_en.png index 83bfb6b921..b515219e5c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8c02eaac714aff002eb259579a33e576fabd4442c2c4528df36ef5c910a9efb -size 37262 +oid sha256:5d1b3e8eabbbd4ebfb3dd14e67aa0944d7d291688d458423c4bb45c47056c028 +size 37280 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 a6c7b2d4d7..0c6d1d86c3 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:0637ff1ee39762445cc8c5498680152c2e2e8012108231fcc2974326a1d8cdb7 -size 41466 +oid sha256:ff548b1c9c9d4fc8b376ae12be6c8468483715726bc0deecf4fc4f5854d727ad +size 41418 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_MultiAccountSection_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_MultiAccountSection_Day_0_en.png new file mode 100644 index 0000000000..caa492261c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_MultiAccountSection_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b77e3ce009dcfac0e41e7f28e5beb934ce39ade1ca85115912a5029b46f47f0 +size 58278 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_MultiAccountSection_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_MultiAccountSection_Night_0_en.png new file mode 100644 index 0000000000..06ecd42cb4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_MultiAccountSection_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4228cd39352d4efcee45866839ff4b1c63426bfd45af083af7b2f57286c2181f +size 59227 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 3024f2cff8..aca3254639 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:a3329563aa6c438335aaadb18cc2767d78eb16c34b616c5e9b14f0d8b66fb032 -size 38106 +oid sha256:bea2b1b58e31957bfef5a6317753a11b4ef34477858af0ab9a515a57f837af76 +size 39307 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 a23da63542..ecd34d97af 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:a7b3d3f464858c9a298461aad3d3a329ac894fbd6206cbc73ec0de16d20e69e3 -size 37946 +oid sha256:44d8cececc9cf14291f2a23a762ed8628139b5fb3f15090b6ca0a2e733a3ac5a +size 39137 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 fe9ddfe32f..1c61185e1d 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:f0ba38204b90b53b0b03efabc5dd69200089327c22250e9416d8ee2fd2b94537 -size 38915 +oid sha256:61c4546a82519138144a2f1ab82f5200a0469fe428b06e2691f443e04df37ba6 +size 40217 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 694ad9d65e..b3452cc94f 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:cb9b09711d4538aa350d9c072b31becb223cd2b842d606f405b208e3cc13b77c -size 38968 +oid sha256:2ae3fa88e84abbc6d83ff55b613758118c03a37a99d7e95cc40369e5222edc2e +size 40266 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 8cb0a46709..31eb958563 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:1b0c1728d598c5bf307a2b9a193c2746831c83de892fc53ccb7da1bb183b8297 -size 8475 +oid sha256:6d4663e9c2c439b05d6d1abe7ae7098539f5263461e9051b0b97305a31b5aa33 +size 8395 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 979b3141be..947b0607e5 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:8e23c1347af06b2fa6f1982159cdb92b2bed278ab1b4caf782c3916520681d75 -size 26616 +oid sha256:5365eb3dd7e10fde8591e6d5ee6dfadef88a9fa25339309ce08445f89af88789 +size 29187 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 b8f30f125d..6faf40fe01 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:9de3402e8b3eed6005f80ecaeea325c533a0f4d51e2dd306baf6ad97a09042d4 -size 23373 +oid sha256:5c66ca10f770fa2f29716f7c737be0acef03e364e9a27a85b81e4ef63e768b6f +size 24012 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 0c7e27a22d..26a56fec35 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:a3f39d62131784e82104ee0033e4da9e2de45eacc1527b878fdaa9dd64d45aa2 -size 8324 +oid sha256:c5b7cb8b7ef8b58a5fa77b7c1c6a911798940337ea9646383ee0cb082dc68f7e +size 8263 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 7c9d79d83e..a6bed8f351 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:809913705eb237bdd8575a92be89cf616d27c7d54a32894378f693dbf1969d0a -size 24731 +oid sha256:c02fa02a7f5787d375ffa6e9bb69cde0b23ca87512ee8120d318becd6de13bef +size 26763 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 6d2ed82c26..a0b5cf90f0 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:d1f5a7d25ad07bc6b0025ea4f600609f58b881452e826d93378f80062626b3d7 -size 21274 +oid sha256:5852e500de1f27083f033726a89d2dda594497d2af7fbe843091cdbd06ddd975 +size 21628 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 new file mode 100644 index 0000000000..1f179c4be8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a809a8792d519f93df9a15c57c06d3fd4f3cb7c84245e80cf61ff07ab1c5d620 +size 20363 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 new file mode 100644 index 0000000000..f326451237 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:433e3467cfa0c0f4e737ec15a1c41d6bbc36d4d2c2dd9f2f7f22bb457208f280 +size 19180 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 ac4547d7ea..e2f67353d2 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:a8747685a776c4fc59d8fed90b057ab18b8abf2670cd430ca4afcf745a402724 -size 44114 +oid sha256:146e40d680e33105cc06cda3a113432f29b42bf7f3b539b4ea17fd6ff9a9265e +size 44093 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 ce722d74ff..f6dad027b0 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:a775ed56c60c2a57cdd8b2d4c54cf557919be0b80948a50402445c37806fd319 -size 42168 +oid sha256:49f72fe1ede6eb11c2e1572af27086df4f9c7eec45cf3ea3cfe6235ea89f12cf +size 42150 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png index 93944ff960..edec0550ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7065059b0ee8165b631225f5468fd937c217b08544ec38bdb13d46dd0bda8a24 -size 30646 +oid sha256:e4063555b9c55be91104f51d5696383aba383d2b7b8d6ff490058f0228e45f81 +size 30659 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png index 77921fc181..14d1396ac9 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2071ff6a7820d23d20cfd6863b9cfa05439f596f9a94c734806d807d661bb813 -size 29383 +oid sha256:c09808dc63bad05bc97a6d5d5f62a4d0fd3db86c17e4662aa2b56eb50143361a +size 29415 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png index 2d0471e281..9c6513bfd1 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:517ee043e2f70702df7791d8b352598a6d7ff572ab5c7d42c606597b3efe2c35 -size 28815 +oid sha256:3a7da5666d690d5d9a6be61225b169e4703626ebc15f2f8f4f3e98d2e322de39 +size 28831 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png index b067bdbc8c..9f70cf870f 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77d65977a990b8505d61bc61d39ea6855e5e931cb0503a380464df9eee4f5854 -size 27528 +oid sha256:97d7e385717dd0ebd7beac1fbd5c205e13f7785febabee15bfffa0026b8d530c +size 27616 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 ecae8555cf..30a1a4772b 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:e4582d19a955cf012c6c07db728b69ae4a1e99dff7292f1dcaaf68d8afbe3141 -size 34821 +oid sha256:4cea381806f7903d56640938ccf8c6d3312c8d27cd768629209db4f8a4b2fb75 +size 34891 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 705fe313f0..f661545528 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:c1f6f4d016188b72e28ae192c9b2379a0734e34586295e4ab5a522fabb152f58 -size 32356 +oid sha256:8a8e1b4d80a8144e92f047259047104c39e3e03ddb9f49ce8b643f5ee074b772 +size 32384 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 26c609c792..e999349e08 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:74ff9446fb276504638707c4779e3947147d40c9114ee126ad22d1977b5d5397 -size 54561 +oid sha256:1b50f2f588c835adb9d2025fd904e8cd9462fb5b5f6952249b3b1fc0098e7dd2 +size 54688 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 f095c5ecda..790641766b 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:2ffd2de03c34e9329cdb7320b67133e5e9f66e3cb133c3be2cb2101de3a789dc -size 52502 +oid sha256:2584a9ff1ae31f50e2c7902e2a04995c7c96d8085328e262d50be8a2395f71f9 +size 52541 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 6b6971c495..b7b6ca3318 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:3e631c4ab7338e9c5fff80de7b5981a85d495b47977d10318a6d02c381d6049e -size 48632 +oid sha256:0a8cf71b7c7ac794bf6f0fd71fe477cb0dd651ff86ad649fe4eaa9e8df83b1cf +size 48594 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en.png index 23adbc3c79..0d260a0a24 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0091ef6aaa0eb66f155e13993d6979578ea27464d28397af9b7587fbc715942 -size 35688 +oid sha256:098b7a3b50e1bca8c03c55c8b118694339b6cc9033f4f94847c66a59e0629743 +size 35835 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 32fab22a28..8e3198caa9 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:7bba3b6776ec442d113b8cade307624406f608b09ca2dddad1ce07a35259d664 -size 45896 +oid sha256:a31fc85a97482f83147f8db4b3dc95ac5c96d16fbcc849ab44588bc2462377d7 +size 45963 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en.png index 6b69fa0ceb..e1c6673840 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1091a682fa4b15d85e557a5681b502da7c9c13de54090125b5256d976ef7322 -size 32707 +oid sha256:01d4b4598591373cbb507fd4074aafc7a0c04d84bf89ff1bc7e4eaa7bde71a7a +size 32880 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 6b6971c495..b7b6ca3318 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:3e631c4ab7338e9c5fff80de7b5981a85d495b47977d10318a6d02c381d6049e -size 48632 +oid sha256:0a8cf71b7c7ac794bf6f0fd71fe477cb0dd651ff86ad649fe4eaa9e8df83b1cf +size 48594 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en.png index 78b88b0ef7..da055ab499 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a2502351e4de7541a573b2d5b0f51643ea90dcad1c654acbcb342660df5fc98 -size 34306 +oid sha256:f4317eb9258f74756d4ecf3aa430dad3ee93cf446588d6fac9bbc6cc12c2c3b0 +size 34467 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 32fab22a28..8e3198caa9 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:7bba3b6776ec442d113b8cade307624406f608b09ca2dddad1ce07a35259d664 -size 45896 +oid sha256:a31fc85a97482f83147f8db4b3dc95ac5c96d16fbcc849ab44588bc2462377d7 +size 45963 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en.png index 4c2d58030a..b556abffc7 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f8a48e08a9caf55dffd4812091f24c26995dcb238944cd409c93c0ef359cebb -size 31493 +oid sha256:d233d12ec3b268a3b0f4b902a784708dc4ddbf804b37d72111c5b7677e346ade +size 31620 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_0_en.png new file mode 100644 index 0000000000..b3abe80b38 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5219016a0af4e1e4f7703dcef3400bf030444845dc0ef52f084e69963170bf1e +size 13951 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_1_en.png new file mode 100644 index 0000000000..c7a2988917 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:964aff2fc49b9d71ebec506bc9acc5010071efca01ed95f240a65b2c91f0f3b1 +size 15852 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_2_en.png new file mode 100644 index 0000000000..8be29a4767 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2097486621a733b1662c02849ec71423d00f9c97c679996fafc8f20a4ef27ce +size 43844 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_3_en.png new file mode 100644 index 0000000000..3b29fd1488 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:daff7333af912c8a0d7f009ba10d044aaa7cdfea2dfa3ccff2fd97209bf3278e +size 44225 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_4_en.png new file mode 100644 index 0000000000..6ffb98cf91 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa05867ac79a3bfb9f59bf1dafb02185274b46e9a5fbf27415a0e907780a0493 +size 36393 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_5_en.png new file mode 100644 index 0000000000..aba8f0ea75 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e74abc389c25fa9a5d33ade6d0f8a2242aeb5150fe81a26e26e9b561c46bb802 +size 43010 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_6_en.png new file mode 100644 index 0000000000..3191bb9c6e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5c8bdf8d389cccb4cced29aa854a70860e9b44cbdde2a25ab95ccd5bc5e1674 +size 39222 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_7_en.png new file mode 100644 index 0000000000..774c1e01c3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbf5d7c5320daa0d6b3c6678a6766c7143bc877850257e2c6923b174dfa9b6d4 +size 34565 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_8_en.png new file mode 100644 index 0000000000..38f435b1b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:298db58a3f80f4194c5710ceeabec0e49b0e80861b931693d58b1a17f4394b4e +size 13873 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_0_en.png new file mode 100644 index 0000000000..1c82d3955f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b982dc465fd47dbce6f88321f80c5390ecc90f7e8ad59a76dd44b6c2b9b80c8a +size 13923 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_1_en.png new file mode 100644 index 0000000000..485fa05bb5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00b13582a4a8a380cc502ca2bb181c1ee7c9f2e1685e1d2a75817107d5ebea99 +size 15393 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_2_en.png new file mode 100644 index 0000000000..a52fe36b2e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ad6ae058adb3decb4733c74ad40e4efdd33744eea3e9a793e92fc6b4cbc1464 +size 42784 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_3_en.png new file mode 100644 index 0000000000..225298ea51 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad30950d847bd769e19ce67d8944c16145e929c52210cd54b1ebc5966efe89b5 +size 43221 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_4_en.png new file mode 100644 index 0000000000..d7a024d955 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6465f03be41ae02635cbdc94f521059bdd6476c5b2ccfca6f28ccd8898112cea +size 35530 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_5_en.png new file mode 100644 index 0000000000..171d67f826 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af017745220027708f5a84ca639c9990fe7e474a38aaeecc3cba82011d195360 +size 42080 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_6_en.png new file mode 100644 index 0000000000..ea980ce184 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73f85bf9ec6c6682e293db2c740d206f6d895d79b3f3d30b967ad55aedbb268c +size 37895 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_7_en.png new file mode 100644 index 0000000000..55995f7fd6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49e2727c42d334c539ee14aab396cadd68b1290e6a256ec2f677f7e4801b30e9 +size 32677 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_8_en.png new file mode 100644 index 0000000000..a3e293c5f5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.leave_LeaveSpaceView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ef494178398bc0f8e7721b7dfde669da4d723aeafa0d4ca6975a58215193416 +size 13848 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png new file mode 100644 index 0000000000..b952647d5d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bad1ef12ad0fe54c78eb91b56bf6f5528a25fd0e79cfa38976535d97f179dae2 +size 15999 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png new file mode 100644 index 0000000000..55266468a5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1ef7cc669d9e180c220aa6e2d7d69a16be61569c98dbb9e81f41e23d6ecaafd +size 20072 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png new file mode 100644 index 0000000000..3b2e097630 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d5111d6ea6b79a4085b65c5d0390d8bbe8d200cc8e50bd9f3cd568ac9ab6179 +size 53740 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png new file mode 100644 index 0000000000..7b987b6d2c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:377cf0bfc11eb067767b089cc5f35ff92307e3512eb30b6f9d51780acf590f9d +size 52891 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png new file mode 100644 index 0000000000..eb53e9990e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbe6f27a97a23f24221a89362cd745dedba4ce6de25b93e26117a340b6d565ca +size 15811 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png new file mode 100644 index 0000000000..66d341ac37 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5798743a5ee59f6842297439ce338652225d8aa18aebebd3aa1785f06fa4863 +size 19723 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png new file mode 100644 index 0000000000..f018c0a4a5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75d18c001040426b961c5f7d0115a2dbad7e3cd1eb8a7af09357cdda1cc591a4 +size 52470 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png new file mode 100644 index 0000000000..dab60e5012 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26e17ed257ded386bf0538c68f6848841f28d4ffd04a8d3f7d3b41feb203deb2 +size 51517 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 f8e38d2a72..8208ba5369 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:d345035d1ee105f7fa1702a3f953df99b0bfce2b0cf48cc92e6248d80690e4b5 -size 14582 +oid sha256:537781aec261b622c47345b48e8fbfd68e3cf0817bb36bf0e34e83769d6d6d21 +size 24337 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 63775e9552..7b095b8e0e 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:15b1f5afddbd729f1ec01326280b11e00f2b7c84e816bd980eac96dc61db92f2 -size 13970 +oid sha256:9e5bf90db9ac805e4054c8d9c759181768a61f13bd30092bdf98fe0a962144f8 +size 23678 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png new file mode 100644 index 0000000000..87517cc357 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2eecd4b52a31208444d14b5017e42fc31df0391bb0ead311c01c9359a7e42f03 +size 74367 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 71543a83b5..b0381d75ac 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:6c767fefbe84a0dd681e07c5ec88903283b2d0d4ef9d6f33232f7ef03976c96d -size 40179 +oid sha256:4aca42b6dc3864028a7896eaa5f410e68a320bfc55dbebd1662b9c080b2c22ca +size 42100 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 index aec01d4046..9ff94627ab 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5407076d84792877400cc065e57b0b2b71d4abc8138c295178328211e1ca341 -size 29011 +oid sha256:897c738d61c159cfe70380e92617b014947dd8d683467b741562271cbf838234 +size 23227 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 71543a83b5..b0381d75ac 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:6c767fefbe84a0dd681e07c5ec88903283b2d0d4ef9d6f33232f7ef03976c96d -size 40179 +oid sha256:4aca42b6dc3864028a7896eaa5f410e68a320bfc55dbebd1662b9c080b2c22ca +size 42100 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 3319bf23d9..61dcd44038 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:bdd93fd698635ce7be6667280182994e3427cd0701d283cce163ec706e88ae95 -size 36420 +oid sha256:f0d95c8515dfe38a3de02fb68a035e41bedac03abadae44e371a85e6f32c9174 +size 38460 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 6f05eab9e5..3accdf3380 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:78507911cefb2a6c7ab9b0ddf1a7a55bc4c13adadbd3a60075ca7eb89969a8b7 -size 32351 +oid sha256:74ecd0909c13895cd8d2e3827fb05fba98bd61f315735b281fd5502a9ae02bd2 +size 32354 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 b6b5c102d2..bf8e3849c5 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:7e54c3911419f23bd99fd879d0b6339cb09cffed560a33aeded5b45e30602df5 -size 46617 +oid sha256:a832cdb8459b626779d19424282b52624e3e76c1943d0e5131049f27fe45e0d8 +size 46376 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 bf207c9c12..39eacc3707 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:d83764f58970b1c90bcdcef4bcda989c762380cc6eb423101d1abfda9ea7fe2f -size 40295 +oid sha256:a5a1132f3b99b4a047839e752ffc6675df91f5cdb750625ac6a15483b7a376a2 +size 40058 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 beaf5aa590..c1cb6c04d8 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:1e23231089f2a3701070fbf279aec7e68febba7cec643a57d2d0525d4eb6e2d9 -size 39087 +oid sha256:5a9c13276724d2343111a065f4f6a3798206f6bc6016dc5314c87448ab74efeb +size 40891 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 index d97ff31e63..00fa43c729 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:000b932dfe770251974ef682e329769ba4412ffa91e46cc5151a16399eb34eda -size 28446 +oid sha256:ada343f41dd5f20ddef032216f462d851aff0d9a94f7545e72062e8b316683f1 +size 22570 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 beaf5aa590..c1cb6c04d8 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:1e23231089f2a3701070fbf279aec7e68febba7cec643a57d2d0525d4eb6e2d9 -size 39087 +oid sha256:5a9c13276724d2343111a065f4f6a3798206f6bc6016dc5314c87448ab74efeb +size 40891 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 c9137d4f70..7d7faefc03 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:3c7b951739149e45d35d7dd2a03893c2471b37d37f5a70e98e14f225697e80a4 -size 35609 +oid sha256:18e41a828de1e73f0909449c096bc7d9660e5b677075845657267db731b913ab +size 37597 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 26386330fc..43f97a426c 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:f268e84b9d477429d817fc4b80ff99fbaff45434e236bae0ce05d656b4337675 -size 31620 +oid sha256:6242746ae0ba35d6e2e4a9c1d4c2a5e9433015eb7711767342afe4386edecfa1 +size 31701 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 d7f9669419..4ea7e1d206 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:33c3a1f2cc919fab50dd20351c69e5d81bd913f316bf6e6c6acf212e64433c3e -size 45581 +oid sha256:1d512f3d1029d30265688b126064ac79fddb295fe7cc14f1e3bc198eca9788ab +size 45377 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 86eb0abea4..3d570edca4 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:5e4abe4b9891455b398be36822531749989569144a7ac4e810bb596aabeac2dc -size 39521 +oid sha256:05710ee6f0dec48f80a7e2c861b3f56208a707bb20ffa94878b56e855dc56da4 +size 39302 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 index a9883b14f4..f721955fe2 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8b6abacdc676b6920df40aedae9dd0792d5a5dfa61e67e5a97429f3ddd38073 -size 27798 +oid sha256:061efadad844117ed72a1bf22e2c61687bc01a93a8e8f290a12b48410713c0f0 +size 26555 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 index b6b5c102d2..bf8e3849c5 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e54c3911419f23bd99fd879d0b6339cb09cffed560a33aeded5b45e30602df5 -size 46617 +oid sha256:a832cdb8459b626779d19424282b52624e3e76c1943d0e5131049f27fe45e0d8 +size 46376 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 index bf207c9c12..39eacc3707 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d83764f58970b1c90bcdcef4bcda989c762380cc6eb423101d1abfda9ea7fe2f -size 40295 +oid sha256:a5a1132f3b99b4a047839e752ffc6675df91f5cdb750625ac6a15483b7a376a2 +size 40058 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 index 89ba6e9fac..1bc1cba469 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9cc65aa4aa06e78016a3fa384d2cd45722bc41abf1de888071fdba400e87b792 -size 27140 +oid sha256:83c1691efa952ea64b00fc794bb082b4975c037d8ada71053a352528c688cd8a +size 25903 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 index d7f9669419..4ea7e1d206 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33c3a1f2cc919fab50dd20351c69e5d81bd913f316bf6e6c6acf212e64433c3e -size 45581 +oid sha256:1d512f3d1029d30265688b126064ac79fddb295fe7cc14f1e3bc198eca9788ab +size 45377 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 index 86eb0abea4..3d570edca4 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e4abe4b9891455b398be36822531749989569144a7ac4e810bb596aabeac2dc -size 39521 +oid sha256:05710ee6f0dec48f80a7e2c861b3f56208a707bb20ffa94878b56e855dc56da4 +size 39302 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 index 866ac11528..d5cbd5e217 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84870e3b5554f97ff8093286bbf246b74b3bfa787a8318304740f55492809721 -size 12397 +oid sha256:43836f421a5f9ab68e4a3f6b3430a6d3daeec9f737bc1b20940a3fc9597057c3 +size 12451 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 index cd0a2208f4..ee9175a640 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72e7183662d01486545e6702faf22dd867cc47ed72a50b8cd2c24a307cef58c9 -size 12408 +oid sha256:053fe3e5699e98280bd40008a26f92a64e34c0f3bda1a52434137745e1ed9cf2 +size 12360 diff --git a/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Day_0_en.png new file mode 100644 index 0000000000..17cefd666f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:215df53e6287ac32df6ec3a4b910a40035c8a516e89bdfc7b23aa1d45320ab78 +size 8285 diff --git a/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Day_1_en.png new file mode 100644 index 0000000000..815ac1db6d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:848f7d81f4bede07d697ad326a0d23845c8446ad49cedc81e985044ad4b45a5c +size 49048 diff --git a/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Night_0_en.png new file mode 100644 index 0000000000..46a95f8bd7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6bfaebba98b5b9bf083e5d29eb9a2a835ce774e26956b582d01b54a1799e507 +size 8172 diff --git a/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Night_1_en.png new file mode 100644 index 0000000000..8a250d9439 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87003f7f0c981f0fee9f0773914e2fd99e18f1001045c13f52bc33dfba2905d6 +size 49941 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_MembersCountMolecule_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_MembersCountMolecule_Day_0_en.png 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_MembersCountMolecule_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_MembersCountMolecule_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_1_en.png new file mode 100644 index 0000000000..4ecd0ac898 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d510dce684941c35ef1b6ea670156e2e5d5ed0eee3577c433e02ca073e0e84c5 +size 8615 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_2_en.png new file mode 100644 index 0000000000..7369d4db69 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:577579f2ed5d7ffb0d289f91d98b13b1df2f9846fefd9c494522809b18de1854 +size 8225 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_3_en.png new file mode 100644 index 0000000000..7e3f2857f1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f053948660fad8b021a4b22d9cd234cde1cce72c3165c812000bd97b0c2da440 +size 7156 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_4_en.png new file mode 100644 index 0000000000..5835ffc88e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66c4f53b8a2b81285243a162c56521d2cb011ece4e725b9c2152d498b9f5a382 +size 5563 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_1_en.png new file mode 100644 index 0000000000..5ab3c70401 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:400a14cd9e24e55351cace9c44432d94d5a81b827a19dff8e61154193e13393a +size 8879 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_2_en.png new file mode 100644 index 0000000000..02f28d9d30 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31a781b781a1dccbf15012c2171f0467e54675c3b6c79740b63b9dc878324754 +size 8500 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_3_en.png new file mode 100644 index 0000000000..3978d80a92 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65cd68f9efcac21733ad327afa3308c09b8e1eece9c0d4323e7c7261a4cd3370 +size 7502 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_4_en.png new file mode 100644 index 0000000000..083f8a317f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTopRtl_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9f18b956d5280c31a4fae4754ddd8c2300e37580dd477b06ed043e198b08fa3 +size 5655 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_1_en.png new file mode 100644 index 0000000000..ad5437d3ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf2ad9276a0aed1b9a97067a4ce60938b0fd556050db36cf28631ce0d69a8c36 +size 8588 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_2_en.png new file mode 100644 index 0000000000..92d02ba84b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:223be8c6d34b8a19758575c7798ca6d925efb391c1ceabd0a1ed4c472364dcbb +size 7823 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_3_en.png new file mode 100644 index 0000000000..a78614c3df --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6e439c9cd3b611cb8beac629d4d6e4da8927d05199aefec9eabe3e77ee67fe4 +size 7187 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_4_en.png new file mode 100644 index 0000000000..5835ffc88e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66c4f53b8a2b81285243a162c56521d2cb011ece4e725b9c2152d498b9f5a382 +size 5563 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_1_en.png new file mode 100644 index 0000000000..25dda15a06 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb3cec5cf746f7faf350c3bc8cb919e5ba0e207ff87959e817e86d82e8531d72 +size 8831 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_2_en.png new file mode 100644 index 0000000000..05f3548b6d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:489cf3807e915dff5ff19458166ab12379002edc42f745e28ae40d151316ed72 +size 8143 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_3_en.png new file mode 100644 index 0000000000..122b31372c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc8bd17e8101352a0bdf542b1fe2ef0de85aa44f78afeb582d7ee18af4d9af75 +size 7491 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_4_en.png new file mode 100644 index 0000000000..083f8a317f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowLastOnTop_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9f18b956d5280c31a4fae4754ddd8c2300e37580dd477b06ed043e198b08fa3 +size 5655 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Day_0_en.png new file mode 100644 index 0000000000..8872565657 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:781862888b73f9bb0948758783a3987d2e04ac2e0690996cdc913b09e4d7283c +size 8645 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Day_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Day_1_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Day_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Day_2_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Day_2_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Day_2_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Day_3_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Day_3_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Day_3_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Day_4_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Day_4_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Day_4_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Night_0_en.png new file mode 100644 index 0000000000..f3df67ab02 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21edc3161193318e2b1ca0a6b1136b76616011b3eb3bf0a3308f81b9c9038303 +size 8957 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Night_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Night_1_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Night_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Night_2_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Night_2_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Night_2_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Night_3_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Night_3_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Night_3_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Night_4_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRowRtl_Night_4_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRowRtl_Night_4_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Day_0_en.png new file mode 100644 index 0000000000..a6f19f722c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7dc701122587804c0e8fe5e238dbfdddd0c93cb3a15c50eab2b0439a8b0a8f86 +size 8638 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Day_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Day_1_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Day_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Day_2_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Day_2_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Day_2_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Day_3_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Day_3_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Day_3_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Day_4_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Day_4_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Day_4_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Night_0_en.png new file mode 100644 index 0000000000..0cf7f398fa --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25262fd5004a941229d6404d73cbacb8c10b4c009b519acb07584eada553bf22 +size 8946 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Night_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Night_1_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Night_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Night_2_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Night_2_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Night_2_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Night_3_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Night_3_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Night_3_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Night_4_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_AvatarRow_Night_4_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_AvatarRow_Night_4_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_100_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_100_en.png new file mode 100644 index 0000000000..eee5b83831 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_100_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9640e8e6d758a03f995c7fbbb6c3c123109594138a7cc53d5416ef5e3e157693 +size 18006 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_101_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_101_en.png new file mode 100644 index 0000000000..70c84280d6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_101_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbe699fb17947981c53d47a9570da71e2388ec493a3a9471aaa13a5405075069 +size 23421 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_102_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_102_en.png new file mode 100644 index 0000000000..f4e93c12ae --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_102_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c6688901f89d3858ec67dda889fe589fb6b59202c64a4b700c2aa484e19f4cd +size 17606 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_103_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_103_en.png new file mode 100644 index 0000000000..53dbe79213 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_103_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26b9908bbd388321444037dcc1aae55037d3dabc7a9f9b14c39ba871f4f9d593 +size 16128 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_104_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_104_en.png new file mode 100644 index 0000000000..9c9b3d4717 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_104_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a96b6d95b8941d80035b309444b9eaa038098fb16aa84dec209fc3ee215ac9e +size 21687 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_105_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_105_en.png new file mode 100644 index 0000000000..7f55d277d5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_105_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8da97746633081690a09a79e3f9974257bbe0f292ad5b8ee2eed454c1273c01e +size 19747 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_106_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_106_en.png new file mode 100644 index 0000000000..91a2bc23da --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_106_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1aaa84f80b3691ed83308966e53deb8dbc97c7943d2f68a8036a6ead603ea627 +size 18270 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_107_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_107_en.png new file mode 100644 index 0000000000..a2cd42670a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_107_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddaae721cac21a188dc3862e3e14e80eeb13c92826bccfb8364ecbd72948d9bd +size 23653 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_108_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_108_en.png new file mode 100644 index 0000000000..885a613f50 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_108_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:207d569640554b3b11cadb03a231ae0dfd85a3c470695cc8428cb2e295d98776 +size 18858 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_109_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_109_en.png new file mode 100644 index 0000000000..4279c36d24 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_109_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68ae347075a6bef936e9c905a12b6b561a01737ab886436d73f4987783a1b45f +size 17541 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_110_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_110_en.png new file mode 100644 index 0000000000..1ff47d58e0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_110_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8caa1fbeb5250684363a21549cdcfff60489059c9d1f0ca115503e63675b3511 +size 22394 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_111_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_111_en.png new file mode 100644 index 0000000000..7946dc4e91 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_111_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce1aba18ea8a6b45d9c40de9af961952e261c7c6200ab13cdfa67906d995208b +size 14888 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_112_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_112_en.png new file mode 100644 index 0000000000..0b12b366b9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_112_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a4d295ae71ba2709845312b84983b79348069ded46b30091b9fa769a587cbb7 +size 14337 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_113_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_113_en.png new file mode 100644 index 0000000000..556cb141b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_113_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da8f54591a52475c6f47fa083a1bb397e1164dd3e735562388fde4ff45644150 +size 16275 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_114_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_114_en.png new file mode 100644 index 0000000000..d19cf477f7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_114_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:157bbbc4511b29b531d85f465c473b7ac186af5f697dfd6ffb6da5a02bb0ca02 +size 16960 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_115_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_115_en.png new file mode 100644 index 0000000000..59c1e3c6ec --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_115_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf17a6c4cd39061bd34e616ec10440d47826a6af81033b99c3a1fb1060e95563 +size 16237 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_116_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_116_en.png new file mode 100644 index 0000000000..4e70c3836d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_116_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:992430d8297f30891aeaf61b054660fc4a4d1c125b06f7df4acc860beda79611 +size 18890 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_117_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_117_en.png new file mode 100644 index 0000000000..9b1636f3bd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_117_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab6848052b8bb306bbe8bbd87a62599047f7e530b70a17417b7ba51f2d90ecf5 +size 14278 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_118_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_118_en.png new file mode 100644 index 0000000000..73fb759161 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_118_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ddb1b57c91c8d3adec9b79307d52a36ac4516e7020f2c3eafc9343fd9d9e368 +size 13536 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_119_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_119_en.png new file mode 100644 index 0000000000..73fbe1fcdf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_119_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f80fd46fb18b92e462e079647b42f6cb8cd101f900120d4927d3101defe2dd36 +size 16213 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_12_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_12_en.png index 70fbf9958f..2d919b097f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_12_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:526485d4efe3b100a450885ab3620f180e587cd9fbe6e8a373032db14483a90b -size 19284 +oid sha256:98a3562697e9d2d7f60a61976a8261b934dcfb75d195d9cf783f6cce4171dfc1 +size 16643 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_13_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_13_en.png index 033d7dc4a3..c2db38fc0e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_13_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f52dd1f56c484022be9b2d33c7647f2b4942096fc41928c8021dbb4b60588f81 -size 18429 +oid sha256:49ca1d5ef80f7572378ef40e7d73b1b669bcac515b3ef54042a7b67f9c3280d7 +size 15403 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 fddc99a32d..0e85620594 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:3a1e82c60b70e7755a1e514d69c0da3c7a3ef93e8528f303e9478eb880c2b51f -size 21531 +oid sha256:bc5b79a8448cbbd9dd73dc416c8b6d051e335845da2caedcdb9fdc3414c4f9c6 +size 20024 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_15_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_15_en.png index 87bd8ecde1..70fbf9958f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_15_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3e85c12d1152986e307ae40753453f5caf789bbfdc00bbc084dbe08f744f4e9 -size 17424 +oid sha256:526485d4efe3b100a450885ab3620f180e587cd9fbe6e8a373032db14483a90b +size 19284 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_16_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_16_en.png index 3fb58209e6..033d7dc4a3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_16_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2c7116dcf6b9ef6a7281b9dccfdfa92589869e897d7d9781c78c3f422d75533 -size 16118 +oid sha256:f52dd1f56c484022be9b2d33c7647f2b4942096fc41928c8021dbb4b60588f81 +size 18429 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 60da20ee58..fddc99a32d 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:2cb816c3e37b3a8c1e0d31a2d7ec05344e7bd16ccd214bb15475754013f0927d -size 21038 +oid sha256:3a1e82c60b70e7755a1e514d69c0da3c7a3ef93e8528f303e9478eb880c2b51f +size 21531 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_18_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_18_en.png index cf80fc6a8c..87bd8ecde1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_18_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_18_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33e206508e839a7517f0621a79571e52c61b225f57efab0379a9bdb510634351 -size 19053 +oid sha256:a3e85c12d1152986e307ae40753453f5caf789bbfdc00bbc084dbe08f744f4e9 +size 17424 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_19_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_19_en.png index 7c518a18dc..3fb58209e6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_19_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_19_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d896fcf527b98ae449f52bdaa77ef5a230756e3723681afdc549f7aa1ba11c42 -size 16867 +oid sha256:c2c7116dcf6b9ef6a7281b9dccfdfa92589869e897d7d9781c78c3f422d75533 +size 16118 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 012289346d..60da20ee58 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:ed5b27869ebf46171cbedb8c57d0163965d77d362445d1edd2ae307371c756ab -size 24687 +oid sha256:2cb816c3e37b3a8c1e0d31a2d7ec05344e7bd16ccd214bb15475754013f0927d +size 21038 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_21_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_21_en.png index d00929d9af..cf80fc6a8c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_21_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_21_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ef22b332b3383732def98b137c6986afcc3414e5e7bcf5b568956bd8f1b5c8c -size 14443 +oid sha256:33e206508e839a7517f0621a79571e52c61b225f57efab0379a9bdb510634351 +size 19053 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_22_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_22_en.png index 2dcb1b35fd..7c518a18dc 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_22_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_22_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ef70634dbed00e908fb43116fcb8d6e2c7084f90f36cca32b83498cf07e1f6a -size 13606 +oid sha256:d896fcf527b98ae449f52bdaa77ef5a230756e3723681afdc549f7aa1ba11c42 +size 16867 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 25626e212d..012289346d 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:aaa1497201d95eb9b39dba91326c179f0d2e1a58ea70772067d46e0e7dcc0d0c -size 16677 +oid sha256:ed5b27869ebf46171cbedb8c57d0163965d77d362445d1edd2ae307371c756ab +size 24687 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_24_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_24_en.png index d4d332376b..d00929d9af 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_24_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_24_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f05acb090eb967a126bb1c4741f763bc7ba254e2d32641d7f6b7d0c3888f5f44 -size 16522 +oid sha256:2ef22b332b3383732def98b137c6986afcc3414e5e7bcf5b568956bd8f1b5c8c +size 14443 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_25_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_25_en.png index 9c52eff9b0..2dcb1b35fd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_25_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_25_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:408498059aa91ae83ec3617172dd1af1ace78c99b172c3da1895d125965a4bd6 -size 15272 +oid sha256:2ef70634dbed00e908fb43116fcb8d6e2c7084f90f36cca32b83498cf07e1f6a +size 13606 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 8c2a67e4f4..25626e212d 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:884eb2d69af3e1d91fddcbcd7173ac547530e5f3439ad2edc3df6cdbbc5ac312 -size 19914 +oid sha256:aaa1497201d95eb9b39dba91326c179f0d2e1a58ea70772067d46e0e7dcc0d0c +size 16677 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_27_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_27_en.png index 196009c9b9..d4d332376b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_27_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_27_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35db9f05aa3c3226c611cd83b9f6f9dd72b70c9b6bb4f68ebb8e0c596b30c99a -size 17591 +oid sha256:f05acb090eb967a126bb1c4741f763bc7ba254e2d32641d7f6b7d0c3888f5f44 +size 16522 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_28_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_28_en.png index b737f4f7eb..9c52eff9b0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_28_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_28_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74de00644b39d999afc93936939e3ff2066f5b9b526d59bad20c50e318ee765e -size 16280 +oid sha256:408498059aa91ae83ec3617172dd1af1ace78c99b172c3da1895d125965a4bd6 +size 15272 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 6a7544bd7b..8c2a67e4f4 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:2e4ac9b255de3602f03a854b8ba71703176253a5c2eeacb364a6c87e89e8f193 -size 21200 +oid sha256:884eb2d69af3e1d91fddcbcd7173ac547530e5f3439ad2edc3df6cdbbc5ac312 +size 19914 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_30_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_30_en.png index 59021750d6..196009c9b9 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_30_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_30_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c4335667e4c604526427826e5d56a09dad85a423844f974f55e6ebda17d42a0 -size 16617 +oid sha256:35db9f05aa3c3226c611cd83b9f6f9dd72b70c9b6bb4f68ebb8e0c596b30c99a +size 17591 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_31_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_31_en.png index 329801a74d..b737f4f7eb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_31_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_31_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27df1354079b41fbed84791777d9e54198c3576418f6d5027b4b88ff22454584 -size 14821 +oid sha256:74de00644b39d999afc93936939e3ff2066f5b9b526d59bad20c50e318ee765e +size 16280 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 ec92a214e4..6a7544bd7b 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:ad1df854d799ccb28358aa5a7c61a7ec4627b6b766813c9d3a3a6b7d423c5a1a -size 21401 +oid sha256:2e4ac9b255de3602f03a854b8ba71703176253a5c2eeacb364a6c87e89e8f193 +size 21200 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_33_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_33_en.png index 06ba756fbc..59021750d6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_33_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_33_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d07f11e284078387bf9ab84e6b79fd35371486ba79fca7ccdfde6e316d7549f -size 14371 +oid sha256:2c4335667e4c604526427826e5d56a09dad85a423844f974f55e6ebda17d42a0 +size 16617 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_34_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_34_en.png index 4c0dfae73b..329801a74d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_34_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_34_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05090ff82f6f6fe27e9a2986b47c4aae72c28c0335532534d9a4b65b00613d24 -size 13634 +oid sha256:27df1354079b41fbed84791777d9e54198c3576418f6d5027b4b88ff22454584 +size 14821 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 4046107dc2..ec92a214e4 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:732c17b7a00cfde8e52aaefe9b02bc009ec840d2cf72adbbd9bbab010cc2b12b -size 16295 +oid sha256:ad1df854d799ccb28358aa5a7c61a7ec4627b6b766813c9d3a3a6b7d423c5a1a +size 21401 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_36_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_36_en.png index 72cb85f820..06ba756fbc 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_36_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_36_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:005c5f7cf69f2452d6fe894e02ae030aa2d6692181b0e94552b33064e755fbec -size 14999 +oid sha256:2d07f11e284078387bf9ab84e6b79fd35371486ba79fca7ccdfde6e316d7549f +size 14371 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_37_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_37_en.png index 390215d29e..4c0dfae73b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_37_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_37_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9c435ebe2e1c7b729c96e75ffa57e488b157a0e7632be7123cb0f4542770a1d -size 14259 +oid sha256:05090ff82f6f6fe27e9a2986b47c4aae72c28c0335532534d9a4b65b00613d24 +size 13634 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 9ade25cdb2..4046107dc2 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:00680313760f229b844a3109530e9c0691b3b02917b3f5e18ac0c8b07b09724c -size 16914 +oid sha256:732c17b7a00cfde8e52aaefe9b02bc009ec840d2cf72adbbd9bbab010cc2b12b +size 16295 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_39_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_39_en.png index 7ae4d9bdba..72cb85f820 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_39_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_39_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:989b61bfa802cbf66168c6484366b85d0f683416c0b8ad6c2c59b7a45ed19313 -size 15436 +oid sha256:005c5f7cf69f2452d6fe894e02ae030aa2d6692181b0e94552b33064e755fbec +size 14999 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_40_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_40_en.png index 32f6867147..390215d29e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_40_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_40_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe2ac65f4be2460a563a07cc64e7b592efb850c9a8d72c26adf6d326bdd200d9 -size 15094 +oid sha256:e9c435ebe2e1c7b729c96e75ffa57e488b157a0e7632be7123cb0f4542770a1d +size 14259 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 795d04f314..9ade25cdb2 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:33b2f35b4be757434e86bce5062551a5ef5af4760016c1751b984e333342841f -size 16373 +oid sha256:00680313760f229b844a3109530e9c0691b3b02917b3f5e18ac0c8b07b09724c +size 16914 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_42_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_42_en.png index a50251b367..7ae4d9bdba 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_42_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_42_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd18a15bc49e87b8abc9231a13771a0fe34003b11fe1fd48df2d91f54bc40cc8 -size 15564 +oid sha256:989b61bfa802cbf66168c6484366b85d0f683416c0b8ad6c2c59b7a45ed19313 +size 15436 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_43_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_43_en.png index 2ec96aeb8a..32f6867147 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_43_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_43_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:388a1ac9f1790fd305fd329b73ae621093d323db3fa4d4d32eef92f8dd5b51ec -size 14824 +oid sha256:fe2ac65f4be2460a563a07cc64e7b592efb850c9a8d72c26adf6d326bdd200d9 +size 15094 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 c07dc719a8..795d04f314 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:bc90e78d2fa94425322895028f1156bfda196ee2a761231920c6ffd80f02984b -size 17512 +oid sha256:33b2f35b4be757434e86bce5062551a5ef5af4760016c1751b984e333342841f +size 16373 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_45_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_45_en.png index 33fa6946a2..38de31dfeb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_45_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_45_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74fda6437495995876f76dc1ff0e056de4937ef8d719b3be1ce73baccd31516e -size 15889 +oid sha256:3d5ed3a4c0340d904404c1ef68e7391e1303f865d15e88de6f82ed68e8dc4b4c +size 19463 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_46_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_46_en.png index 2a2ed97c47..7024a8cc93 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_46_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_46_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04fb2f230a09f0d3e27e151d98aa5d91e416b5ca9637b4db5d8e00118a68e7c4 -size 15166 +oid sha256:c44464ab02dd9f1fc0624f2110bc9938c8aa8856bdad811aafa05eff2e144e4a +size 18884 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 233b0baf35..a9d29c9fc3 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:896f698ef11d8a0577baf0a81cb54cabc307d95d05c230e5e3bdde40c3dc0900 -size 17844 +oid sha256:57145b520f6702934571122f4c243f8740658847c0c60e6f901179d1d420d16a +size 20857 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_48_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_48_en.png index 20cd179751..a50251b367 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_48_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_48_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf264b6f8af5b7432511d595ff8c663c5e2be33c9f85268627f5188e3f0f8db0 -size 18949 +oid sha256:fd18a15bc49e87b8abc9231a13771a0fe34003b11fe1fd48df2d91f54bc40cc8 +size 15564 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_49_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_49_en.png index 304c73c954..2ec96aeb8a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_49_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_49_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bbe1e3e8e1ea119cc61509617d3fa8e2bd047f619c41271901c73c46be1d610 -size 18201 +oid sha256:388a1ac9f1790fd305fd329b73ae621093d323db3fa4d4d32eef92f8dd5b51ec +size 14824 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 397c9c40c1..c07dc719a8 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:7a7c183808801645e285dfba563c036c204a347de20b4b1e40fcfeab29fafb7d -size 20876 +oid sha256:bc90e78d2fa94425322895028f1156bfda196ee2a761231920c6ffd80f02984b +size 17512 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_51_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_51_en.png index 559903c1ac..33fa6946a2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_51_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_51_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a48eb99f4466e3a883c0c7441006f201e0db22fd0e920ad0a77a8512637e01bb -size 16445 +oid sha256:74fda6437495995876f76dc1ff0e056de4937ef8d719b3be1ce73baccd31516e +size 15889 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_52_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_52_en.png index 42cab93666..2a2ed97c47 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_52_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_52_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:895ba9a35b8854d5005fc0671a7e6cbba5e26525b4a8ed4fa0fb612432caa04e -size 15205 +oid sha256:04fb2f230a09f0d3e27e151d98aa5d91e416b5ca9637b4db5d8e00118a68e7c4 +size 15166 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 bb704942db..233b0baf35 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:98baed819cd8b08085ceabf7dbccccc77b0fdf0d28a3f852879f6f8aa02ee441 -size 19848 +oid sha256:896f698ef11d8a0577baf0a81cb54cabc307d95d05c230e5e3bdde40c3dc0900 +size 17844 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_54_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_54_en.png index 7f42bdc985..20cd179751 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_54_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_54_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4678c15b608547c255d5eeb128c144ae7a3c5a21de2b047fc77f15364822ac2 -size 12847 +oid sha256:cf264b6f8af5b7432511d595ff8c663c5e2be33c9f85268627f5188e3f0f8db0 +size 18949 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_55_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_55_en.png index efb2ef5625..304c73c954 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_55_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_55_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e30395594c1a35d90fc0a04af340eb79cbe686d90856414d0024b769f86e89d -size 12507 +oid sha256:0bbe1e3e8e1ea119cc61509617d3fa8e2bd047f619c41271901c73c46be1d610 +size 18201 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 d49a629afa..397c9c40c1 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:60362244c7adf3644ed2ae354643e0525aad2a4454f6b13abce8fd265f4987c6 -size 13777 +oid sha256:7a7c183808801645e285dfba563c036c204a347de20b4b1e40fcfeab29fafb7d +size 20876 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_57_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_57_en.png index c7da5ce09d..559903c1ac 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_57_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_57_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73de8ee7933c1a45a266761b14e09b008a62dc69b1600bbc69b8a01370fb97c1 -size 18641 +oid sha256:a48eb99f4466e3a883c0c7441006f201e0db22fd0e920ad0a77a8512637e01bb +size 16445 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_58_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_58_en.png index f9e9999e71..42cab93666 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_58_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_58_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b62b7dc22bfc727cfcbf7f6a19683bcf4da116c7192f9943f5addd11c39fcadb -size 17018 +oid sha256:895ba9a35b8854d5005fc0671a7e6cbba5e26525b4a8ed4fa0fb612432caa04e +size 15205 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 0f4930cb81..bb704942db 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:c0855112283bf797bc846e1982c6293e321361368481a803e12658723aaf8409 -size 22988 +oid sha256:98baed819cd8b08085ceabf7dbccccc77b0fdf0d28a3f852879f6f8aa02ee441 +size 19848 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_60_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_60_en.png index b7e63845b6..7f42bdc985 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_60_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_60_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:049b3ac784a8e400bad374a960ecb77d7f4bca81079be764dc74cb161f7a1093 -size 20880 +oid sha256:c4678c15b608547c255d5eeb128c144ae7a3c5a21de2b047fc77f15364822ac2 +size 12847 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_61_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_61_en.png index 517de9d5bd..efb2ef5625 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_61_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_61_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21d83d719bee5fd19ad090791c7848890d87e4bd9eaddb17c9da1773ec81667f -size 19249 +oid sha256:5e30395594c1a35d90fc0a04af340eb79cbe686d90856414d0024b769f86e89d +size 12507 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 e39f788f2a..d49a629afa 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:edc743a5cd067eb13e464f27c2f12f95433bd020dd1b3e920614ee913e06e476 -size 25008 +oid sha256:60362244c7adf3644ed2ae354643e0525aad2a4454f6b13abce8fd265f4987c6 +size 13777 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_63_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_63_en.png index 71ba87addd..c7da5ce09d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_63_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_63_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45e6821d622bfef22cc41a040e12e2b49e0bb50a88b0a143bdb25e3368809412 -size 16496 +oid sha256:73de8ee7933c1a45a266761b14e09b008a62dc69b1600bbc69b8a01370fb97c1 +size 18641 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_64_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_64_en.png index c00f6f274d..f9e9999e71 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_64_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_64_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ddd2399d3d701fa962dc5683e054c2c4829ccd99d04498415ce88487f5e0ec8 -size 15760 +oid sha256:b62b7dc22bfc727cfcbf7f6a19683bcf4da116c7192f9943f5addd11c39fcadb +size 17018 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 c6917d9519..0f4930cb81 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:4293d98ed31f87c59ff5741344c2e41f58ff8f19c9342f6626553e978dbfc674 -size 18429 +oid sha256:c0855112283bf797bc846e1982c6293e321361368481a803e12658723aaf8409 +size 22988 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_66_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_66_en.png index 439c8e754f..b7e63845b6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_66_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_66_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37d1e302d4a610aaab7cfc6a15572b7eb92b059d9384e7bbe4daebcaf2da9049 -size 21343 +oid sha256:049b3ac784a8e400bad374a960ecb77d7f4bca81079be764dc74cb161f7a1093 +size 20880 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_67_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_67_en.png index 19347bbaaa..517de9d5bd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_67_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_67_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:536d8d899945483352137d23356f3777db8e2225288f01e8cbcd1a770ca7f3ee -size 20494 +oid sha256:21d83d719bee5fd19ad090791c7848890d87e4bd9eaddb17c9da1773ec81667f +size 19249 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 3a5d6c57ad..e39f788f2a 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:542b080ae9374e4f6d57689b1e1b6a84a0f81f017bd276bbcab43b9659be4335 -size 23534 +oid sha256:edc743a5cd067eb13e464f27c2f12f95433bd020dd1b3e920614ee913e06e476 +size 25008 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_69_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_69_en.png index c116d68c19..71ba87addd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_69_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_69_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aeb02390258eade3ceb8c437bcf3592f3dde463d6f1b2a43ffefd61abf4184c4 -size 17236 +oid sha256:45e6821d622bfef22cc41a040e12e2b49e0bb50a88b0a143bdb25e3368809412 +size 16496 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_6_en.png index 0622e6c69b..1bf67b7c98 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f52fa29e031ab362baed77ac18228960d67eb5d6bdf909b6c1fb3777f5eacc5 -size 19629 +oid sha256:8cb143e42d0bc07652d74c74ba641a5332ee9bebb071fa5efba59ecacad04be5 +size 21857 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_70_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_70_en.png index 04ec60caeb..c00f6f274d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_70_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_70_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5c6bfe22fb71a9c6353718c2124000245182359ac1c43dfc8dd9b91415e66e6 -size 16385 +oid sha256:6ddd2399d3d701fa962dc5683e054c2c4829ccd99d04498415ce88487f5e0ec8 +size 15760 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 e74e8a07b8..c6917d9519 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:408f90e8a2f4dcd0716118fb522e5cd4adba0b2a429723fc2e47555b82acf005 -size 19501 +oid sha256:4293d98ed31f87c59ff5741344c2e41f58ff8f19c9342f6626553e978dbfc674 +size 18429 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_72_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_72_en.png index 84b6c56c0e..439c8e754f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_72_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_72_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9778edbbb33ab0bbaa0072bd4659488937efe0f8b8fac0a5b64497fe6a1e28e2 -size 20838 +oid sha256:37d1e302d4a610aaab7cfc6a15572b7eb92b059d9384e7bbe4daebcaf2da9049 +size 21343 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_73_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_73_en.png index aa9b310163..19347bbaaa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_73_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_73_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97f43860bf9e65c7822943f8806a0642ce59957be5e35650e2b69a2c562a694c -size 18637 +oid sha256:536d8d899945483352137d23356f3777db8e2225288f01e8cbcd1a770ca7f3ee +size 20494 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 3e54c2ace5..3a5d6c57ad 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:77e71b006b71300ccb1005ad3cac6a93ae242ff03f2936fdf9d10c0604d20faa -size 26121 +oid sha256:542b080ae9374e4f6d57689b1e1b6a84a0f81f017bd276bbcab43b9659be4335 +size 23534 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_75_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_75_en.png index b2eb2208d7..c116d68c19 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_75_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_75_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e744d3fe3315dc6cb5beab80dc29d565500367772339071575b6505370e47207 -size 14771 +oid sha256:aeb02390258eade3ceb8c437bcf3592f3dde463d6f1b2a43ffefd61abf4184c4 +size 17236 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_76_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_76_en.png index 5dd29ba5c0..04ec60caeb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_76_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_76_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:448ea365e3e75059bc888961e3454562d35ab116c75b3b9855723b5fc1480d92 -size 14028 +oid sha256:d5c6bfe22fb71a9c6353718c2124000245182359ac1c43dfc8dd9b91415e66e6 +size 16385 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 4985f409fd..e74e8a07b8 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:ad98105fcd6d6c02e5ae036ad49770b4aa35d94e32ab3a63f040deccc2375e6e -size 16703 +oid sha256:408f90e8a2f4dcd0716118fb522e5cd4adba0b2a429723fc2e47555b82acf005 +size 19501 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_78_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_78_en.png index e34b1d369a..84b6c56c0e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_78_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_78_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b8e747d51d6a6ab1ab6ff80224d0241a16f38b39e0c156dc6eef47ea56eca63 -size 18140 +oid sha256:9778edbbb33ab0bbaa0072bd4659488937efe0f8b8fac0a5b64497fe6a1e28e2 +size 20838 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_79_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_79_en.png index fcf2962f34..aa9b310163 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_79_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_79_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34f28c6dbbe094b9ba070c6f1ef4b10b2625ea094629b6305dff70bdf6367ddc -size 16894 +oid sha256:97f43860bf9e65c7822943f8806a0642ce59957be5e35650e2b69a2c562a694c +size 18637 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_7_en.png index dccb49ff50..3db35531c4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b6a0bdd2239b473f541e95588f2531a2761d0d9872cda61cdd87e2e195b14cf -size 17425 +oid sha256:5eecc64fbbf67e53578ba6c974905c16d000a5dc8b2e0fa99c41e46dea36424b +size 19651 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 8e996d615c..3e54c2ace5 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:ccfe44da86dce16ba4816cfe2029c05d317fe29ca3bb816278f2bd929298d855 -size 21532 +oid sha256:77e71b006b71300ccb1005ad3cac6a93ae242ff03f2936fdf9d10c0604d20faa +size 26121 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_81_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_81_en.png index 54558de104..b2eb2208d7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_81_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_81_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5b77bec92bc7c4da3fdc72831797d685bf042131c2222351cc7c852da48cd60 -size 17662 +oid sha256:e744d3fe3315dc6cb5beab80dc29d565500367772339071575b6505370e47207 +size 14771 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_82_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_82_en.png index d36db47fcc..5dd29ba5c0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_82_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_82_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99f2b0e2fda05d6fa16c0f2dcc52e92f6fa1c656c6d84cf03e8507a5c14104e8 -size 16932 +oid sha256:448ea365e3e75059bc888961e3454562d35ab116c75b3b9855723b5fc1480d92 +size 14028 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 e0660d56c3..4985f409fd 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:e79cb41e125705af70990dec3e3fdc0a0202ec8d62f2077eb2a298243ae2e94d -size 19624 +oid sha256:ad98105fcd6d6c02e5ae036ad49770b4aa35d94e32ab3a63f040deccc2375e6e +size 16703 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_84_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_84_en.png index bc91269c49..e34b1d369a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_84_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_84_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0faccb345f65a543c56e4db44714614915a62b6916a77a0f4adfdae5215311e -size 15128 +oid sha256:5b8e747d51d6a6ab1ab6ff80224d0241a16f38b39e0c156dc6eef47ea56eca63 +size 18140 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_85_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_85_en.png index 259e239462..fcf2962f34 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_85_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_85_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d626da2924325c074dcd2baf1c112430399553a9ee652cac03b0b1d3db4cff3 -size 14393 +oid sha256:34f28c6dbbe094b9ba070c6f1ef4b10b2625ea094629b6305dff70bdf6367ddc +size 16894 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 2082b4df3a..8e996d615c 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:2be7c565d30e9f45a583b93ec948a7cb35a4bc872c4f479bd1efe9eda8972e7c -size 17044 +oid sha256:ccfe44da86dce16ba4816cfe2029c05d317fe29ca3bb816278f2bd929298d855 +size 21532 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 index 6dfa192fb9..54558de104 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c28eae20f614eae53e09aa4e396792f36ee1478c8141faad788fe397502da237 -size 20671 +oid sha256:e5b77bec92bc7c4da3fdc72831797d685bf042131c2222351cc7c852da48cd60 +size 17662 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 index b740d5c979..d36db47fcc 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ada8d054436b15e9029150eb99e78103050f8d0428a1c78ef5667bfd54d3be5 -size 19195 +oid sha256:99f2b0e2fda05d6fa16c0f2dcc52e92f6fa1c656c6d84cf03e8507a5c14104e8 +size 16932 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 index 6b06753f70..e0660d56c3 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7586db3dc8687c5c5fc73d28dd968041b192c160286f6a7c881eb7d86a2d1bd2 -size 24477 +oid sha256:e79cb41e125705af70990dec3e3fdc0a0202ec8d62f2077eb2a298243ae2e94d +size 19624 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 d1b6ea7f02..3f0469f5cc 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:3898d22d9de94323eb04d5385af9a1084434c521f8b51f5af74ef1159dd2d1cc -size 25138 +oid sha256:c425ffed237de0e71451baf51f674051c3ab6481ecceeff15c7f2f73193fd3ab +size 26866 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 index 42b0241e10..bc91269c49 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ef75decb9f501eb6c1941e212e1b330203c16458bfc2e10bfac3d197b091a35 -size 17116 +oid sha256:e0faccb345f65a543c56e4db44714614915a62b6916a77a0f4adfdae5215311e +size 15128 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 index 04732e27fb..259e239462 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4940205ca9411e3175d24d7a45a49df77b2187318d4245d902db75230838e26f -size 15869 +oid sha256:5d626da2924325c074dcd2baf1c112430399553a9ee652cac03b0b1d3db4cff3 +size 14393 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 index a48e1ad7d3..2082b4df3a 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1faa9029414b146707f2cd0d7c4899355cd196c3b5acd6e95ebe0d0bb599003 -size 20504 +oid sha256:2be7c565d30e9f45a583b93ec948a7cb35a4bc872c4f479bd1efe9eda8972e7c +size 17044 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_93_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_93_en.png new file mode 100644 index 0000000000..6dfa192fb9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_93_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c28eae20f614eae53e09aa4e396792f36ee1478c8141faad788fe397502da237 +size 20671 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_94_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_94_en.png new file mode 100644 index 0000000000..b740d5c979 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_94_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ada8d054436b15e9029150eb99e78103050f8d0428a1c78ef5667bfd54d3be5 +size 19195 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_95_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_95_en.png new file mode 100644 index 0000000000..6b06753f70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_95_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7586db3dc8687c5c5fc73d28dd968041b192c160286f6a7c881eb7d86a2d1bd2 +size 24477 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_96_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_96_en.png new file mode 100644 index 0000000000..42b0241e10 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_96_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ef75decb9f501eb6c1941e212e1b330203c16458bfc2e10bfac3d197b091a35 +size 17116 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_97_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_97_en.png new file mode 100644 index 0000000000..04732e27fb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_97_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4940205ca9411e3175d24d7a45a49df77b2187318d4245d902db75230838e26f +size 15869 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_98_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_98_en.png new file mode 100644 index 0000000000..a48e1ad7d3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_98_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1faa9029414b146707f2cd0d7c4899355cd196c3b5acd6e95ebe0d0bb599003 +size 20504 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_99_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_99_en.png new file mode 100644 index 0000000000..3eeb6f47e4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_99_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90982bf0b723b89c9b070c1dc94eb5a9fcf66623c382d496174f026e053929fb +size 19493 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ProgressDialogWithContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ProgressDialogWithContent_Day_0_en.png new file mode 100644 index 0000000000..d4ef5f016f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ProgressDialogWithContent_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b637e70da73ea0fca81adb861abf69fb2092d35493417fb29103f5acebf6fa0 +size 11706 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ProgressDialogWithContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ProgressDialogWithContent_Night_0_en.png new file mode 100644 index 0000000000..7e648f0530 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ProgressDialogWithContent_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:440b6f0e84c9fccfbbc997a40e1e8459df1962218fa4fdceb7ea8ecc8c96090b +size 11442 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ProgressDialogWithTextAndContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ProgressDialogWithTextAndContent_Day_0_en.png new file mode 100644 index 0000000000..b58b42b2d6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ProgressDialogWithTextAndContent_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3212b09963d5fa9b89b1ebfd8f904b3f436399299e67f560f39620d063eb997 +size 11408 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ProgressDialogWithTextAndContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ProgressDialogWithTextAndContent_Night_0_en.png new file mode 100644 index 0000000000..a534eae610 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ProgressDialogWithTextAndContent_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae69faa0c95f4ae4b7b410da438f0e3992571c21837e644e63621fa1f73e297a +size 11220 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_DialogWithVeryLongTitleAndIcon_Dialog_with_a_very_long_title_and_icon_Dialogs_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_DialogWithVeryLongTitleAndIcon_Dialog_with_a_very_long_title_and_icon_Dialogs_en.png new file mode 100644 index 0000000000..460e856f24 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_DialogWithVeryLongTitleAndIcon_Dialog_with_a_very_long_title_and_icon_Dialogs_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b77f17f245a60b74cfff9e28cb0f5d0251172332582e00f6bcf2767c867bcd8 +size 56167 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_DialogWithVeryLongTitle_Dialog_with_a_very_long_title_Dialogs_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_DialogWithVeryLongTitle_Dialog_with_a_very_long_title_Dialogs_en.png new file mode 100644 index 0000000000..f4a7fb3f40 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_DialogWithVeryLongTitle_Dialog_with_a_very_long_title_Dialogs_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1a1350f33dd50756aa7aa175e1dbcc42c5922d82d4b020a5556c4e0d1db0b83 +size 63540 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableOrgAvatarRtl_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableOrgAvatarRtl_Day_0_en.png new file mode 100644 index 0000000000..7bff485c2a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableOrgAvatarRtl_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b9bd735000eda1054294863bdff79d32b391e1511f025bdc05ba4374be3f27c +size 31769 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableOrgAvatarRtl_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableOrgAvatarRtl_Night_0_en.png new file mode 100644 index 0000000000..8b36a5e259 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableOrgAvatarRtl_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8da76de0773fc62f7eb73dd16756d6ccad8229db121eb39d43f7c27d4ab8b48b +size 31339 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableOrgAvatar_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableOrgAvatar_Day_0_en.png new file mode 100644 index 0000000000..1771b0fbd9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableOrgAvatar_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a7773c27cc488967a4c8c526f4d19ca3a78e15c1efa2f7267881c2e9798d5c4 +size 31223 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableOrgAvatar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableOrgAvatar_Night_0_en.png new file mode 100644 index 0000000000..5b5950e4c3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableOrgAvatar_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b5b92e446d5968fb115e49b2b2337a46a9a7f43577413924e7a1b1f3b752646 +size 30692 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_OrganizationHeader_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_OrganizationHeader_Day_0_en.png new file mode 100644 index 0000000000..b4bf34b446 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_OrganizationHeader_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:862a2a93700999b27dbdf461e70d2e97441550795d03ccb3e4ec4f964d3d6d99 +size 41995 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_OrganizationHeader_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_OrganizationHeader_Night_0_en.png new file mode 100644 index 0000000000..02e7b152fe --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_OrganizationHeader_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33f802e03011231b8ef81574b12e9622cf9d3901e3faf0225b8e745e88f50623 +size 41324 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_en.png new file mode 100644 index 0000000000..03b2c21087 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0fec2a25db38f9c65e3fd0218fc5c9937a017b762696dfd7be2906c7cbc28336 +size 19111 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceHeaderRootView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceHeaderRootView_Night_0_en.png new file mode 100644 index 0000000000..a4dc748f4f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceHeaderRootView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5412ef6dc7306f203649b777cb43ce0ef011e65640e84f14b12dd3198f68b992 +size 18204 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceHeaderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceHeaderView_Day_0_en.png new file mode 100644 index 0000000000..7a7bf57ab7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceHeaderView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21acaf9ea606378f5f1e83795e41bec9edc2da05143efba04b70f67cce2c80b0 +size 61626 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceHeaderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceHeaderView_Night_0_en.png new file mode 100644 index 0000000000..e76102c580 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceHeaderView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e20d4dc80e8f983516cde0361333f941818cade431fb38ae318cfa9a589ab0d8 +size 60846 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceInfoRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceInfoRow_Day_0_en.png new file mode 100644 index 0000000000..7138e2aa39 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceInfoRow_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e4684cfbe7611d6c68879ddd7da5c57aa542b9f8b9250caf5f27009cabaadb2 +size 22353 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceInfoRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceInfoRow_Night_0_en.png new file mode 100644 index 0000000000..38e9551bc3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceInfoRow_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c7ad1861890262a67664d4e3b3e28d8a5d9ac4556ad029d1d5a387f61ac513f +size 21275 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Day_0_en.png new file mode 100644 index 0000000000..937800912c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9df673a4073b6806332b3181987f7ff0594eca818ef4a2b760136582d16de476 +size 6191 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Night_0_en.png new file mode 100644 index 0000000000..44709bf2c1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6a4bf38d356974df822f69f6de85b5b83b4622395c98b1c72d2d352b4e820ce +size 6093 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceMembersView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceMembersView_Day_0_en.png new file mode 100644 index 0000000000..a855314980 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceMembersView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0df7c79b75dabc28fbae243d83547bb3786cb36eca0d33cabc38db0c40ffeb90 +size 6993 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceMembersView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceMembersView_Night_0_en.png new file mode 100644 index 0000000000..2226a864e7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceMembersView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1c7cd3392fa7550edf07859f3e5c9a8e69c3e20ea784ac5acffcee5876a4dca +size 7312 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en.png new file mode 100644 index 0000000000..53e8c50c72 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cb722a95b3ee29f751d053ea1cab634c3181b07678449600a2c23a97de0fcd1 +size 16496 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en.png new file mode 100644 index 0000000000..2268d59ba5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:684f50c937b0ab7413cdff653a11ed11796583bf2182bfc1bbc1725f42d3a0ba +size 13073 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en.png new file mode 100644 index 0000000000..b90c8c5ba1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f6b5eaa4a84fc70cab57d37737b390583d3af9f6e829cecffbcf36351bd40e2 +size 23562 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en.png new file mode 100644 index 0000000000..eeafaa0dab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f271f630d93fdf49436509d08276b6a444db92c06fd06129af93cffbb4623d4 +size 17931 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en.png new file mode 100644 index 0000000000..f925a8d461 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9fed82716640b4c96b6ef666d9f992e966c17ebddc275c5f562b412e2d549b0 +size 15065 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en.png new file mode 100644 index 0000000000..8ea41b3e64 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79e7cecc4b03937d2a030721383664d54466e7ce25459c3628ace8b36b305214 +size 35293 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en.png new file mode 100644 index 0000000000..7b1b324268 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7798eeef4a1de3312ec69b757963c6430d3952d45db6346961f79dae4e15913a +size 41389 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en.png new file mode 100644 index 0000000000..7b1b324268 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7798eeef4a1de3312ec69b757963c6430d3952d45db6346961f79dae4e15913a +size 41389 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en.png new file mode 100644 index 0000000000..3663e98d52 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f07736b16ece457794cdbd3859ede26ed0aa4b53acdc69d7f913df9a9b0fad1f +size 16002 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en.png new file mode 100644 index 0000000000..244e0ce786 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad16996462532092686f1638f3ee1f40ede99b5ccb2bc53e57931f50920e62b5 +size 12629 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en.png new file mode 100644 index 0000000000..5085864cc6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:690df48431acad687f2a2e72aebba4fa63de433ed68b7b9e19818a78973cd211 +size 22676 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en.png new file mode 100644 index 0000000000..1e85e954f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34073398379389aa862c892e6eaa33574606071597a0095dfa5c84b99154b5df +size 17218 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en.png new file mode 100644 index 0000000000..45f9f23c79 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13d555688a963f8bf664c8bafc397a48c31575d4a2c2fa06a20b191e620d23ea +size 14587 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en.png new file mode 100644 index 0000000000..4c0e5e6671 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ac86fef06b52c5456c5eb828c1b60615792906f74d15ea35e3289d2cff47ee1 +size 34183 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en.png new file mode 100644 index 0000000000..d2f9e318e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b431eae85dcb3e5efde883a47c78aab6c346429366357bb5f6e740342eeeb97 +size 40072 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en.png new file mode 100644 index 0000000000..d2f9e318e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b431eae85dcb3e5efde883a47c78aab6c346429366357bb5f6e740342eeeb97 +size 40072 diff --git a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_0_en.png index fb72370351..9e034f4fbb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa702b884d741412232c68245a503bd06c9fa595be214a5cf852ea576d77a031 -size 26878 +oid sha256:db93be255fb6e9a70e1814bad2275b4e828924aaf5dfdf623456290d986926e4 +size 26853 diff --git a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_1_en.png index 56fabcb8dd..e8c4accaa2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e70d3bca33fcccf6839b651a8b34056bdcbbacf18447fa8d99638ddc0558816 -size 26011 +oid sha256:ba7cfc134e5843e053b6c0c842b882e4a18505451959510da2b02dc295f68da6 +size 25985 diff --git a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_2_en.png index c138569904..e4163ee256 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdb77ac0053690361d44a221b2290557a8c9b904eb5771c0d4bc76382804d579 -size 26491 +oid sha256:cf678d8b087d3d1ae5ee836ec177828610ad074ff611c8b7f42b4008589bd911 +size 26467 diff --git a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_3_en.png index b1232891b4..32f9d544c8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3de92534cdb971ef27901f9e5e5db4146b39d9527afc6b23daa7718ee4d4ef4 -size 20477 +oid sha256:0dd34239f647ab049a3dc1bf137abd7e31081b4da25fa850ee9ebff89a84cdd4 +size 20460 diff --git a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_0_en.png index a839d4cbc7..b10e2188fe 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:541193db62ebb995d81e48f3f49a74fafa8d537e9dd99c4f7537b7cd14a1258f -size 25632 +oid sha256:bad56fc6d29cf54e760ac8a5d13e53cc047651c2cf1efad36d78dc3956bb7ac5 +size 25638 diff --git a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_1_en.png index d995dccd06..f0a44c0dbc 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ae7e0666a300169a5eabf6e8d8e8846034488e3cfb92e5c2fe7c1cdd889fb13 -size 24880 +oid sha256:42dd21b3fcd9faf803153852e6ed3ea9d43cdc6f637af3d1a34f4892174738eb +size 24886 diff --git a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_2_en.png index d3b91b8c05..f8337097c9 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a32d7138ef227596fda8a0e495143ab255bf30913ce62d1d9f40108cde79728 -size 25303 +oid sha256:4284ff47ebb1fec51df90fc55219f9354c751d3d9c17c2b2d133b57550176b7b +size 25304 diff --git a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_3_en.png index 9951f9bfa8..fafa6d94da 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.permissions.api_PermissionsView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c91ae52ece27098b46e8e787407262d50bbf95c42550b26faeabcc8a382887eb -size 19192 +oid sha256:952550c4bfcc10751a781279f1023b11e42e73b585c97543a212e3d7f0887218 +size 19156 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 3d5088afdc..b50f6ba4a8 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:736b3ea3821b421acc7f1a9bdc54f2c603dea34f3995150cccaef8b15ba7d75f -size 13057 +oid sha256:bda5872074c486c4b40575b814ed2865056c02931ee1ca0f8a98ac4f32e5f985 +size 13093 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 411f8fb2da..00b7d45027 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:8a56cab67985d64487d89b4ccecff21e391c1562edebb28280ee5ab8f26393ad -size 12033 +oid sha256:648ac4cf6333a39bbfb90e8a5faf2aa44a1de19c1613bf754ca6bc4d1974fc8b +size 12019 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 7713f10b20..639d4d89b5 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:886722a012510c51dbad9a50f717d6ba3b64425cad0755c90afdd84a5cae67c0 -size 14618 +oid sha256:7f6e0912e5970104f0dd09d152d6a3da57f7a2b2201aa5dd86c7b691c797edad +size 14637 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 426d15e371..24e9093c2e 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:edc8ef6512ea5f0e39a059d692063455e2e1174a8e781eccd0c2e543d156544a -size 13465 +oid sha256:b879de5d6db9a3cff7de4b2ee779b6c8de456b629f31f07fcb57de3a7c7e7c28 +size 13409 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 560c64ad73..0342d06c2c 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:6ab5214a9d6f424910cd551454392443faf40694b5c3bde3b839fc49e1cd0633 -size 16845 +oid sha256:14d318ad147102d2cdfe73c46125fa35864010ea38dcd6dfbb7fd3b12d64d878 +size 16873 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 6a2334efc0..262699e09c 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:d7bd2c66d3efa336b22df0820ea9e36633a22b36927163d3950088d9179376dd -size 15521 +oid sha256:2ce99713434fba303745a45b2d12d18841c90ab65fb650e700a5fc9e070bd79f +size 15496 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_3_en.png new file mode 100644 index 0000000000..c77ea23f32 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:692bf4dd932e39d3b3d7c2b234524b9e31a288808b1a5c4574e3a0ca19d6a725 +size 23296 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_3_en.png new file mode 100644 index 0000000000..616133a868 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:303c3a69b1ed1bcb8bf2253b8a70f2a8c06171e583f43dbc10023d66880c9e6d +size 21309 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 22eaaced82..2442a17143 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:642e972ecba4cf6af7961066a67b556047d0ab3c35a4ce66e043ba185d93b528 -size 20101 +oid sha256:9051334b3879fe2a6fb56a3c573ecb49eca7700a94184d828e2c0329cbb82b4b +size 25070 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 5b2d1f6f65..40126799db 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:26e11efbeec28f03e9b8d0df822e886b9e420fc523286cc0aa69760f8d5b639c -size 19562 +oid sha256:50fb2b753bdad6a9df4ce2f55a552464e229c7b81be16e0d4f5c8e4e4b3d129b +size 24312 diff --git a/tests/uitests/src/test/snapshots/images/libraries.ui.common.nodes_EmptyView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.ui.common.nodes_EmptyView_Day_0_en.png new file mode 100644 index 0000000000..1b6fb4bab8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.ui.common.nodes_EmptyView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 +size 3642 diff --git a/tests/uitests/src/test/snapshots/images/libraries.ui.common.nodes_EmptyView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.ui.common.nodes_EmptyView_Night_0_en.png new file mode 100644 index 0000000000..d6fd8eeb70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.ui.common.nodes_EmptyView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd +size 3659 diff --git a/tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Day_0_en.png index 08d3f6a214..d15340871f 100644 --- a/tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f181687fc541a13928a35f9a23fca99a6d6bdf7875bd900fe444d001b5256fd -size 19592 +oid sha256:94242ace02e2db7156e0bdb2c3e071255006ed839360db7a3322a8bb0a93458a +size 19570 diff --git a/tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Night_0_en.png index 38a6de5b59..616d21a8a6 100644 --- a/tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81f5b10dd42b27de179693857fd3aeac838562540371be8e789801c69755edc6 -size 18202 +oid sha256:b7ff1f5db38fc99452f0a7c3fc727ee7ffd6461b69ad0d58d126571b1eea2851 +size 18191 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 0aac7e51af..2afebbef4d 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -123,6 +123,7 @@ "includeRegex" : [ "push_.*", "notification_.*", + "troubleshoot_notifications\\.test_blocked_users\\..*", "troubleshoot_notifications_test_current_push_provider.*", "troubleshoot_notifications_test_detect_push_provider.*", "troubleshoot_notifications_test_display_notification_.*", @@ -199,6 +200,12 @@ "screen\\.security_and_privacy\\..*" ] }, + { + "name" : ":features:space:impl", + "includeRegex" : [ + "screen\\.leave_space\\..*" + ] + }, { "name" : ":features:userprofile:shared", "includeRegex" : [ diff --git a/tools/release/release.sh b/tools/release/release.sh index 30d4cea205..59a29dc13c 100755 --- a/tools/release/release.sh +++ b/tools/release/release.sh @@ -64,7 +64,8 @@ fi # 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="36.0.0" +# Read buildToolsVersion from file plugins/src/main/kotlin/Versions.kt +buildToolsVersion=$(grep "BUILD_TOOLS_VERSION =" ./plugins/src/main/kotlin/Versions.kt |cut -d '=' -f 2 |xargs) buildToolsPath="${androidHome}/build-tools/${buildToolsVersion}" if [[ ! -d ${buildToolsPath} ]]; then @@ -145,7 +146,7 @@ printf "Creating fastlane file...\n" printf -v versionReleaseNumber2Digits "%02d" "${versionReleaseNumber}" 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}" +printf "Main changes in this version: bug fixes and improvements.\nFull changelog: https://github.com/element-hq/element-x-android/releases" > "${fastlanePathFile}" read -r -p "I have created the file ${fastlanePathFile}, please edit it and press enter to continue. " git add "${fastlanePathFile}" diff --git a/tools/templates/files/fileTemplates/Template Module Feature Build Gradle Impl.kts b/tools/templates/files/fileTemplates/Template Module Feature Build Gradle Impl.kts index 6d6502418c..d650a1f3e2 100644 --- a/tools/templates/files/fileTemplates/Template Module Feature Build Gradle Impl.kts +++ b/tools/templates/files/fileTemplates/Template Module Feature Build Gradle Impl.kts @@ -1,4 +1,5 @@ -import extension.setupAnvil +import extension.setupDependencyInjection +import extension.testCommonDependencies plugins { id("io.element.android-compose-library") @@ -9,7 +10,7 @@ android { namespace = "io.element.android.features.${MODULE_NAME}.impl" } -setupAnvil() +setupDependencyInjection() dependencies { api(projects.features.${MODULE_NAME}.api) @@ -19,10 +20,6 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) } diff --git a/tools/templates/files/fileTemplates/Template Module Feature Entry Point API.kt b/tools/templates/files/fileTemplates/Template Module Feature Entry Point API.kt index 6297ec4e24..37f0500444 100644 --- a/tools/templates/files/fileTemplates/Template Module Feature Entry Point API.kt +++ b/tools/templates/files/fileTemplates/Template Module Feature Entry Point API.kt @@ -6,7 +6,6 @@ import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint interface ${FEATURE_NAME}EntryPoint : FeatureEntryPoint { - fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder interface NodeBuilder { diff --git a/tools/templates/files/fileTemplates/Template Module Feature Entry Point Flow Impl.kt b/tools/templates/files/fileTemplates/Template Module Feature Entry Point Flow Impl.kt index adfd142ae5..e3089e8d5e 100644 --- a/tools/templates/files/fileTemplates/Template Module Feature Entry Point Flow Impl.kt +++ b/tools/templates/files/fileTemplates/Template Module Feature Entry Point Flow Impl.kt @@ -3,14 +3,15 @@ package io.element.android.features.${MODULE_NAME}.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 dev.zacsweers.metro.ContributesBinding import io.element.android.features.${MODULE_NAME}.api.${FEATURE_NAME}EntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject @ContributesBinding(AppScope::class) -class Default${FEATURE_NAME}EntryPoint @Inject constructor() : ${FEATURE_NAME}EntryPoint { +@Inject +class Default${FEATURE_NAME}EntryPoint() : ${FEATURE_NAME}EntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): ${FEATURE_NAME}EntryPoint.NodeBuilder { val plugins = ArrayList() diff --git a/tools/templates/files/fileTemplates/Template Module Feature Node Flow Impl.kt b/tools/templates/files/fileTemplates/Template Module Feature Node Flow Impl.kt index d08d67ae38..2c23326e0f 100644 --- a/tools/templates/files/fileTemplates/Template Module Feature Node Flow Impl.kt +++ b/tools/templates/files/fileTemplates/Template Module Feature Node Flow Impl.kt @@ -9,18 +9,19 @@ 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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope +import dev.zacsweers.metro.AppScope import kotlinx.parcelize.Parcelize // CHANGE THE SCOPE @ContributesNode(AppScope::class) -class ${FEATURE_NAME}FlowNode @AssistedInject constructor( +@AssistedInject +class ${FEATURE_NAME}FlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, ) : BackstackNode<${FEATURE_NAME}FlowNode.NavTarget>( diff --git a/tools/templates/files/fileTemplates/Template Presentation Classes.kt b/tools/templates/files/fileTemplates/Template Presentation Classes.kt index aa44bc4269..91ec2165f7 100644 --- a/tools/templates/files/fileTemplates/Template Presentation Classes.kt +++ b/tools/templates/files/fileTemplates/Template Presentation Classes.kt @@ -2,9 +2,10 @@ import androidx.compose.runtime.Composable import io.element.android.libraries.architecture.Presenter -import javax.inject.Inject +import dev.zacsweers.metro.Inject -class ${NAME}Presenter @Inject constructor() : Presenter<${NAME}State> { +@Inject +class ${NAME}Presenter() : Presenter<${NAME}State> { @Composable override fun present(): ${NAME}State { diff --git a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.1.kt b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.1.kt index 0c997351f0..5bd0f4fdc4 100644 --- a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.1.kt +++ b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.1.kt @@ -5,14 +5,15 @@ 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.di.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import dev.zacsweers.metro.AppScope // CHANGE THE SCOPE @ContributesNode(AppScope::class) -class ${NAME}Node @AssistedInject constructor( +@AssistedInject +class ${NAME}Node( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: ${NAME}Presenter,