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