diff --git a/.editorconfig b/.editorconfig
index ac30459c21..2ab3cbeae7 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -913,3 +913,8 @@ ij_yaml_sequence_on_new_line = false
ij_yaml_space_before_colon = false
ij_yaml_spaces_within_braces = true
ij_yaml_spaces_within_brackets = true
+
+[**/generated/**]
+generated_code = true
+ij_formatter_enabled = false
+ktlint = disabled
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
index 5f13ff4efb..0a1fc8653d 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,4 +1,5 @@
screenshots/**/*.png filter=lfs diff=lfs merge=lfs -text
+libraries/compound/screenshots/** filter=lfs diff=lfs merge=lfs -text
**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text
**/docs/images-lfs/*.png filter=lfs diff=lfs merge=lfs -text
libraries/mediaupload/impl/src/test/assets/* filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index eaea9a841d..fb14c3cee6 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -36,7 +36,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APKs
diff --git a/.github/workflows/build_enterprise.yml b/.github/workflows/build_enterprise.yml
index 9c4c8cec8f..0d9b5949cc 100644
--- a/.github/workflows/build_enterprise.yml
+++ b/.github/workflows/build_enterprise.yml
@@ -44,7 +44,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug Gplay Enterprise APK
diff --git a/.github/workflows/generate_github_pages.yml b/.github/workflows/generate_github_pages.yml
index a4286491cb..0d03d1a928 100644
--- a/.github/workflows/generate_github_pages.yml
+++ b/.github/workflows/generate_github_pages.yml
@@ -19,7 +19,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
diff --git a/.github/workflows/maestro-local.yml b/.github/workflows/maestro-local.yml
index 64eff695d9..7481bec0ba 100644
--- a/.github/workflows/maestro-local.yml
+++ b/.github/workflows/maestro-local.yml
@@ -34,7 +34,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APK
diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml
index f89373b44f..537565743e 100644
--- a/.github/workflows/nightlyReports.yml
+++ b/.github/workflows/nightlyReports.yml
@@ -27,7 +27,7 @@ jobs:
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: false
@@ -67,7 +67,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Dependency analysis
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 6e9dea69ab..773bc02d93 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -52,7 +52,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
@@ -90,7 +90,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Konsist tests
@@ -130,7 +130,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build Gplay Debug
@@ -174,7 +174,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Detekt
@@ -214,7 +214,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Ktlint check
@@ -254,7 +254,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Knit
diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml
index a4d54afeb4..bbcbef04d8 100644
--- a/.github/workflows/recordScreenshots.yml
+++ b/.github/workflows/recordScreenshots.yml
@@ -40,7 +40,7 @@ jobs:
java-version: '21'
# Add gradle cache, this should speed up the process
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 661110338d..2cce85bd5a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -25,7 +25,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
- name: Create app bundle
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@@ -66,7 +66,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
- name: Create Enterprise app bundle
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@@ -94,7 +94,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
- name: Create APKs
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
diff --git a/.github/workflows/scripts/recordScreenshots.sh b/.github/workflows/scripts/recordScreenshots.sh
index a610c6cc69..19469e69e3 100755
--- a/.github/workflows/scripts/recordScreenshots.sh
+++ b/.github/workflows/scripts/recordScreenshots.sh
@@ -56,6 +56,12 @@ echo "Deleting previous screenshots"
echo "Record screenshots"
./gradlew recordPaparazziDebug --stacktrace $GRADLE_ARGS
+echo "Deleting previous screenshots"
+./gradlew removeOldScreenshots --stacktrace --warn $GRADLE_ARGS
+
+echo "Record screenshots (Compound)"
+./gradlew :libraries:compound:recordRoborazziDebug --stacktrace -PpreDexEnable=false --max-workers 4 --warn $GRADLE_ARGS
+
echo "Committing changes"
git config http.sslVerify false
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
index 080fcef0e0..acfe6ba87f 100644
--- a/.github/workflows/sonar.yml
+++ b/.github/workflows/sonar.yml
@@ -33,7 +33,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build debug code and test fixtures
diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml
index b5548fd2b1..7740af4064 100644
--- a/.github/workflows/sync-localazy.yml
+++ b/.github/workflows/sync-localazy.yml
@@ -18,7 +18,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 55f27aa9ba..22c302cbb3 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -52,7 +52,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
@@ -78,6 +78,7 @@ jobs:
name: tests-and-screenshot-tests-results
path: |
**/build/paparazzi/failures/
+ **/build/roborazzi/failures/
**/build/reports/tests/*UnitTest/
# https://github.com/codecov/codecov-action
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 5377aedff1..e4d138bfd5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,73 @@
+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
=============================
diff --git a/annotations/build.gradle.kts b/annotations/build.gradle.kts
index 66b88b4dfa..00d0735292 100644
--- a/annotations/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/app/src/main/kotlin/io/element/android/x/di/AppModule.kt b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt
index 55d5245735..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
@@ -33,7 +33,6 @@ 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
@@ -107,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 84%
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 8bba1dd50b..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
@@ -9,15 +9,15 @@ package io.element.android.x.di
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
-import io.element.android.appnav.di.RoomComponentFactory
+import io.element.android.appnav.di.RoomGraphFactory
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.room.JoinedRoom
@ContributesBinding(SessionScope::class)
@Inject
-class DefaultRoomComponentFactory(
+class DefaultRoomGraphFactory(
private val sessionGraph: SessionGraph,
-) : RoomComponentFactory {
+) : RoomGraphFactory {
override fun create(room: JoinedRoom): Any {
return sessionGraph.roomGraphFactory
.create(room, room)
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
index e452bc2a9f..e48dd52daf 100644
--- a/app/src/main/kotlin/io/element/android/x/di/RoomGraph.kt
+++ b/app/src/main/kotlin/io/element/android/x/di/RoomGraph.kt
@@ -7,18 +7,15 @@
package io.element.android.x.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.RoomScope
-import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
@GraphExtension(RoomScope::class)
interface RoomGraph : NodeFactoriesBindings {
- @ContributesTo(SessionScope::class)
@GraphExtension.Factory
interface Factory {
fun create(
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
index 255c6c7ab9..3782b00a58 100644
--- a/app/src/main/kotlin/io/element/android/x/di/SessionGraph.kt
+++ b/app/src/main/kotlin/io/element/android/x/di/SessionGraph.kt
@@ -7,8 +7,6 @@
package io.element.android.x.di
-import dev.zacsweers.metro.AppScope
-import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.GraphExtension
import dev.zacsweers.metro.Provides
import io.element.android.libraries.architecture.NodeFactoriesBindings
@@ -19,7 +17,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
interface SessionGraph : NodeFactoriesBindings {
val roomGraphFactory: RoomGraph.Factory
- @ContributesTo(AppScope::class)
@GraphExtension.Factory
interface Factory {
fun create(@Provides matrixClient: MatrixClient): SessionGraph
diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts
index 3ea6f9d1c9..063e26b9da 100644
--- a/appnav/build.gradle.kts
+++ b/appnav/build.gradle.kts
@@ -26,9 +26,11 @@ dependencies {
allFeaturesApi(project)
implementation(projects.libraries.core)
+ implementation(projects.libraries.accountselect.api)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
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)
@@ -36,11 +38,13 @@ 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)
implementation(libs.coil)
+ implementation(projects.features.announcement.api)
implementation(projects.features.ftue.api)
implementation(projects.features.share.api)
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 ef3d4b27b9..290a351e86 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt
@@ -24,7 +24,7 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -41,7 +41,7 @@ import kotlinx.parcelize.Parcelize
* This allow to inject objects with SessionScope in the constructor of [LoggedInFlowNode].
*/
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class LoggedInAppScopeFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
@@ -57,6 +57,7 @@ class LoggedInAppScopeFlowNode(
), DependencyInjectionGraphOwner {
interface Callback : Plugin {
fun onOpenBugReport()
+ fun onAddAccount()
}
@Parcelize
@@ -83,6 +84,10 @@ class LoggedInAppScopeFlowNode(
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 5444b5c465..c557d6e1c2 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt
@@ -31,9 +31,17 @@ class LoggedInEventProcessor(
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 fc4fcc3a7a..b0fc707d07 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
@@ -36,9 +36,8 @@ 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 dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.annotations.ContributesNode
import io.element.android.appnav.loggedin.LoggedInNode
@@ -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,7 @@ import kotlin.time.Duration.Companion.seconds
import kotlin.time.toKotlinDuration
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class LoggedInFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
@@ -138,11 +137,12 @@ class LoggedInFlowNode(
) {
interface Callback : Plugin {
fun onOpenBugReport()
+ fun onAddAccount()
}
private val loggedInFlowProcessor = LoggedInEventProcessor(
- snackbarDispatcher,
- matrixClient.roomMembershipObserver(),
+ snackbarDispatcher = snackbarDispatcher,
+ roomMembershipObserver = matrixClient.roomMembershipObserver,
)
private val verificationListener = object : SessionVerificationServiceListener {
@@ -189,7 +189,7 @@ class LoggedInFlowNode(
// TODO We do not support Space yet, so directly navigate to main space
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
loggedInFlowProcessor.observeEvents(sessionCoroutineScope)
- matrixClient.sessionVerificationService().setListener(verificationListener)
+ matrixClient.sessionVerificationService.setListener(verificationListener)
mediaPreviewConfigMigration()
sessionCoroutineScope.launch {
@@ -218,7 +218,7 @@ class LoggedInFlowNode(
appNavigationStateService.onLeavingSpace(id)
appNavigationStateService.onLeavingSession(id)
loggedInFlowProcessor.stopObserving()
- matrixClient.sessionVerificationService().setListener(null)
+ matrixClient.sessionVerificationService.setListener(null)
}
)
setupSendingQueue()
@@ -281,7 +281,7 @@ class LoggedInFlowNode(
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() {
@@ -366,8 +366,8 @@ class LoggedInFlowNode(
}
}
val spaceCallback = object : SpaceEntryPoint.Callback {
- override fun onOpenRoom(roomId: RoomId) {
- backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
+ override fun onOpenRoom(roomId: RoomId, viaParameters: List) {
+ backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames = viaParameters))
}
}
val inputs = RoomFlowNode.Inputs(
@@ -392,6 +392,10 @@ class LoggedInFlowNode(
}
is NavTarget.Settings -> {
val callback = object : PreferencesEntryPoint.Callback {
+ override fun onAddAccount() {
+ plugins().forEach { it.onAddAccount() }
+ }
+
override fun onOpenBugReport() {
plugins().forEach { it.onOpenBugReport() }
}
@@ -404,11 +408,7 @@ class LoggedInFlowNode(
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)))
}
}
@@ -548,13 +548,6 @@ class LoggedInFlowNode(
}
}
-@ContributesNode(AppScope::class)
-@Inject
-class PlaceholderNode(
- @Assisted buildContext: BuildContext,
- @Assisted plugins: List,
-) : Node(buildContext, plugins = plugins)
-
@Parcelize
private class AttachRoomOperation(
val roomTarget: LoggedInFlowNode.NavTarget.Room,
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 db33ee873e..5da44f715d 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt
@@ -22,7 +22,7 @@ import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -36,7 +36,7 @@ import io.element.android.libraries.matrix.ui.media.NotLoggedInImageLoaderFactor
import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class NotLoggedInFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 3f1d40335d..19290c5f8b 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
@@ -9,23 +9,23 @@ 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 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.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.annotations.ContributesNode
import io.element.android.appnav.di.MatrixSessionCache
@@ -34,18 +34,22 @@ import io.element.android.appnav.intent.ResolvedIntent
import io.element.android.appnav.root.RootNavStateFlowFactory
import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView
+import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
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.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.api.DeeplinkData
-import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
+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
@@ -53,14 +57,16 @@ 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)
-@Inject
+@AssistedInject
class RootFlowNode(
@Assisted val buildContext: BuildContext,
@Assisted plugins: List,
@@ -71,9 +77,12 @@ class RootFlowNode(
private val presenter: RootPresenter,
private val bugReportEntryPoint: BugReportEntryPoint,
private val signedOutEntryPoint: SignedOutEntryPoint,
+ private val accountSelectEntryPoint: AccountSelectEntryPoint,
private val intentResolver: IntentResolver,
private val oidcActionFlow: OidcActionFlow,
private val bugReporter: BugReporter,
+ private val featureFlagService: FeatureFlagService,
+ private val announcementService: AnnouncementService,
) : BaseFlowNode(
backstack = BackStack(
initialElement = NavTarget.SplashScreen,
@@ -95,27 +104,24 @@ class RootFlowNode(
}
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) {
@@ -137,20 +143,17 @@ class RootFlowNode(
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 = sessionStore.getLatestSessionId()
if (latestSessionId == null) {
@@ -172,45 +175,64 @@ class RootFlowNode(
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)
+ announcementService.Render(Modifier)
}
}
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 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))
}
@@ -226,32 +248,46 @@ class RootFlowNode(
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 onDone() {
backstack.pop()
}
}
- bugReportEntryPoint
- .nodeBuilder(this, buildContext)
- .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()
+ }
}
}
@@ -267,19 +303,29 @@ class RootFlowNode(
}
private suspend fun onLoginLink(params: LoginParams) {
- // Is there a session already?
- val latestSessionId = sessionStore.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")
}
}
@@ -290,56 +336,95 @@ class RootFlowNode(
// 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()
}
}
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 78b9617eae..ce80ac7207 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
@@ -109,7 +109,10 @@ class MatrixSessionCache(
}
private fun onNewMatrixClient(matrixClient: MatrixClient) {
- val syncOrchestrator = syncOrchestratorFactory.create(matrixClient)
+ val syncOrchestrator = syncOrchestratorFactory.create(
+ syncService = matrixClient.syncService,
+ sessionCoroutineScope = matrixClient.sessionCoroutineScope,
+ )
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
matrixClient = matrixClient,
syncOrchestrator = syncOrchestrator,
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/RoomComponentFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/RoomGraphFactory.kt
similarity index 90%
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 fc8f2c175b..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
-fun interface RoomComponentFactory {
+fun interface RoomGraphFactory {
fun create(room: JoinedRoom): Any
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt
index 26377acae8..2561fd3ac7 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
@@ -10,14 +10,15 @@ package io.element.android.appnav.di
import androidx.annotation.VisibleForTesting
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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
import io.element.android.libraries.core.coroutine.childScope
-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
import io.element.android.services.appnavstate.api.AppForegroundStateService
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
@@ -30,23 +31,25 @@ import java.util.concurrent.atomic.AtomicBoolean
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
-@Inject
+@AssistedInject
class SyncOrchestrator(
- @Assisted matrixClient: MatrixClient,
+ @Assisted private val syncService: SyncService,
+ @Assisted sessionCoroutineScope: CoroutineScope,
private val appForegroundStateService: AppForegroundStateService,
private val networkMonitor: NetworkMonitor,
dispatchers: CoroutineDispatchers,
) {
@AssistedFactory
interface Factory {
- fun create(matrixClient: MatrixClient): SyncOrchestrator
+ fun create(
+ syncService: SyncService,
+ sessionCoroutineScope: CoroutineScope,
+ ): SyncOrchestrator
}
- private val syncService = matrixClient.syncService()
-
private val tag = "SyncOrchestrator"
- private val coroutineScope = matrixClient.sessionCoroutineScope.childScope(dispatchers.io, tag)
+ private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag)
private val started = AtomicBoolean(false)
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 05c721360f..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
@@ -14,12 +14,12 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class LoggedInNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 6f04a08f7b..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
@@ -22,7 +22,7 @@ import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.annotations.ContributesNode
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
@@ -64,7 +64,7 @@ import java.util.Optional
import kotlin.jvm.optionals.getOrNull
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class RoomFlowNode(
@Assisted val buildContext: BuildContext,
@Assisted plugins: List,
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 62b10e7a80..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
@@ -25,7 +25,7 @@ import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -45,7 +45,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class JoinedRoomFlowNode(
@Assisted val buildContext: BuildContext,
@Assisted plugins: List,
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 b36a24d4f0..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
@@ -18,9 +18,9 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
-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.features.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
@@ -45,7 +45,7 @@ import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class JoinedRoomLoadedFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
@@ -56,7 +56,7 @@ class JoinedRoomLoadedFlowNode(
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) {
@@ -83,7 +83,7 @@ class JoinedRoomLoadedFlowNode(
private val inputs: Inputs = inputs()
private val callbacks = plugins.filterIsInstance()
- override val graph = roomComponentFactory.create(inputs.room)
+ override val graph = roomGraphFactory.create(inputs.room)
init {
lifecycle.subscribe(
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/SyncOrchestratorTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt
index 2c143df095..f26df83a56 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt
@@ -11,7 +11,6 @@ import io.element.android.appnav.di.SyncOrchestrator
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.matrix.api.sync.SyncState
-import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.tests.testutils.WarmUpRule
@@ -385,7 +384,8 @@ class SyncOrchestratorTest {
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
) = SyncOrchestrator(
- matrixClient = FakeMatrixClient(syncService = syncService, sessionCoroutineScope = backgroundScope),
+ syncService = syncService,
+ sessionCoroutineScope = backgroundScope,
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
dispatchers = testCoroutineDispatchers(),
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixSessionCacheTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixSessionCacheTest.kt
index 47237d5f77..6df80ee95d 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixSessionCacheTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixSessionCacheTest.kt
@@ -10,12 +10,13 @@ package io.element.android.appnav.di
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
-import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.tests.testutils.testCoroutineDispatchers
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -117,9 +118,13 @@ class MatrixSessionCacheTest {
}
private fun TestScope.createSyncOrchestratorFactory() = object : SyncOrchestrator.Factory {
- override fun create(matrixClient: MatrixClient): SyncOrchestrator {
+ override fun create(
+ syncService: SyncService,
+ sessionCoroutineScope: CoroutineScope,
+ ): SyncOrchestrator {
return SyncOrchestrator(
- matrixClient,
+ syncService = syncService,
+ sessionCoroutineScope = sessionCoroutineScope,
appForegroundStateService = FakeAppForegroundStateService(),
networkMonitor = FakeNetworkMonitor(),
dispatchers = testCoroutineDispatchers(),
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt
index 05898c75f3..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
@@ -111,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
@@ -120,7 +120,7 @@ class IntentResolverTest {
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
ResolvedIntent.Oidc(
- oidcAction = OidcAction.GoBack
+ oidcAction = OidcAction.GoBack()
)
)
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 7171d5b079..b7662aeb56 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -15,6 +15,7 @@ plugins {
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.dependencycheck) apply false
+ alias(libs.plugins.roborazzi) apply false
alias(libs.plugins.dependencyanalysis)
alias(libs.plugins.detekt)
alias(libs.plugins.ktlint)
@@ -192,6 +193,21 @@ subprojects {
tasks.findByName("recordPaparazziRelease")?.dependsOn(removeOldScreenshotsTask)
}
+// Make sure to delete old snapshot before recording new ones
+subprojects {
+ val screenshotsDir = File("${project.projectDir}/screenshots")
+ val removeOldScreenshotsTask = tasks.register("removeOldScreenshots") {
+ onlyIf { screenshotsDir.exists() }
+ doFirst {
+ println("Delete previous screenshots located at $screenshotsDir\n")
+ screenshotsDir.deleteRecursively()
+ }
+ }
+ tasks.findByName("recordRoborazzi")?.dependsOn(removeOldScreenshotsTask)
+ tasks.findByName("recordRoborazziDebug")?.dependsOn(removeOldScreenshotsTask)
+ tasks.findByName("recordRoborazziRelease")?.dependsOn(removeOldScreenshotsTask)
+}
+
subprojects {
tasks.withType().configureEach {
compilerOptions {
diff --git a/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt b/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt
index f50a3b6fac..15bb4afbe0 100644
--- a/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt
+++ b/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt
@@ -35,6 +35,7 @@ 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
@@ -71,14 +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.interfaceBuilder(moduleClassName)
+ .addAnnotation(AnnotationSpec.builder(Origin::class).addMember(CLASS_PLACEHOLDER, nodeClassName).build())
.addAnnotation(BindingContainer::class)
- .addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.toTypeName()).build())
+ .addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember(CLASS_PLACEHOLDER, scope.toTypeName()).build())
.addFunction(
FunSpec.builder("bind${ksClass.simpleName.asString()}Factory")
.addModifiers(KModifier.ABSTRACT)
@@ -88,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()
)
@@ -115,7 +118,7 @@ class ContributesNodeProcessor(
val assistedParameters = constructor.parameters.filter { it.isAnnotationPresent(Assisted::class) }
if (assistedParameters.size != 2) {
error(
- "${ksClass.qualifiedName?.asString()} must have an @Inject constructor with 2 @Assisted parameters. Found: ${assistedParameters.size}",
+ "${ksClass.qualifiedName?.asString()} must have a constructor with 2 @Assisted parameters. Found: ${assistedParameters.size}",
)
}
val contextAssistedParam = assistedParameters[0]
@@ -138,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")
@@ -161,6 +165,7 @@ class ContributesNodeProcessor(
}
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/enterprise b/enterprise
index 95789d4011..ffc02b8d0f 160000
--- a/enterprise
+++ b/enterprise
@@ -1 +1 @@
-Subproject commit 95789d40119499eba8a79284df9dd2306405b099
+Subproject commit ffc02b8d0f35188c3ef8a876dc1532bfe3e533da
diff --git a/fastlane/metadata/android/en-US/changelogs/202510000.txt b/fastlane/metadata/android/en-US/changelogs/202510000.txt
new file mode 100644
index 0000000000..e30ec573cc
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202510000.txt
@@ -0,0 +1,2 @@
+Main changes in this version: Spaces!
+Full changelog: https://github.com/element-hq/element-x-android/releases
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 901f3047d1..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
@@ -16,14 +16,14 @@ 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.Inject
+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
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class AnalyticsOptInNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
diff --git a/features/announcement/api/build.gradle.kts b/features/announcement/api/build.gradle.kts
new file mode 100644
index 0000000000..0fa87f039e
--- /dev/null
+++ b/features/announcement/api/build.gradle.kts
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+plugins {
+ id("io.element.android-compose-library")
+}
+
+android {
+ namespace = "io.element.android.features.announcement.api"
+}
diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceEvents.kt b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt
similarity index 62%
rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceEvents.kt
rename to features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt
index 848dac3ebc..21b83e7449 100644
--- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceEvents.kt
+++ b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt
@@ -5,8 +5,9 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.space.impl
+package io.element.android.features.announcement.api
-sealed interface SpaceEvents {
- data object LoadMore : SpaceEvents
+enum class Announcement {
+ Space,
+ NewNotificationSound,
}
diff --git a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt
new file mode 100644
index 0000000000..a98c0af199
--- /dev/null
+++ b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.api
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import kotlinx.coroutines.flow.Flow
+
+interface AnnouncementService {
+ suspend fun showAnnouncement(announcement: Announcement)
+
+ suspend fun onAnnouncementDismissed(announcement: Announcement)
+
+ fun announcementsToShowFlow(): Flow>
+
+ /**
+ * Use this composable to render the announcement UI in Fullscreen.
+ */
+ @Composable
+ fun Render(
+ modifier: Modifier,
+ )
+}
diff --git a/features/announcement/impl/build.gradle.kts b/features/announcement/impl/build.gradle.kts
new file mode 100644
index 0000000000..222d080d6e
--- /dev/null
+++ b/features/announcement/impl/build.gradle.kts
@@ -0,0 +1,37 @@
+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.features.announcement.impl"
+
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+}
+
+setupDependencyInjection()
+
+dependencies {
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.designsystem)
+ implementation(projects.libraries.preferences.api)
+ implementation(projects.libraries.uiStrings)
+ api(projects.features.announcement.api)
+ implementation(libs.androidx.datastore.preferences)
+
+ testCommonDependencies(libs, true)
+ testImplementation(projects.libraries.matrix.test)
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt
new file mode 100644
index 0000000000..432aad4a0a
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.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.announcement.impl
+
+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.announcement.api.Announcement
+import io.element.android.features.announcement.impl.store.AnnouncementStatus
+import io.element.android.features.announcement.impl.store.AnnouncementStore
+import io.element.android.libraries.architecture.Presenter
+import kotlinx.coroutines.flow.map
+
+@Inject
+class AnnouncementPresenter(
+ private val announcementStore: AnnouncementStore,
+) : Presenter {
+ @Composable
+ override fun present(): AnnouncementState {
+ val showSpaceAnnouncement by remember {
+ announcementStore.announcementStatusFlow(Announcement.Space).map {
+ it == AnnouncementStatus.Show
+ }
+ }.collectAsState(false)
+ return AnnouncementState(
+ showSpaceAnnouncement = showSpaceAnnouncement,
+ )
+ }
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt
new file mode 100644
index 0000000000..c8ea728d64
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.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.announcement.impl
+
+data class AnnouncementState(
+ val showSpaceAnnouncement: Boolean,
+)
+
+fun anAnnouncementState(
+ showSpaceAnnouncement: Boolean = false,
+) = AnnouncementState(
+ showSpaceAnnouncement = showSpaceAnnouncement,
+)
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt
new file mode 100644
index 0000000000..6d616f14e9
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import dev.zacsweers.metro.Inject
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.api.AnnouncementService
+import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
+import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementView
+import io.element.android.features.announcement.impl.store.AnnouncementStatus
+import io.element.android.features.announcement.impl.store.AnnouncementStore
+import io.element.android.libraries.architecture.Presenter
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.first
+
+@ContributesBinding(AppScope::class)
+@Inject
+class DefaultAnnouncementService(
+ private val announcementStore: AnnouncementStore,
+ private val announcementPresenter: Presenter,
+ private val spaceAnnouncementPresenter: Presenter,
+) : AnnouncementService {
+ override suspend fun showAnnouncement(announcement: Announcement) {
+ when (announcement) {
+ Announcement.Space -> showSpaceAnnouncement()
+ Announcement.NewNotificationSound -> {
+ announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
+ }
+ }
+ }
+
+ override suspend fun onAnnouncementDismissed(announcement: Announcement) {
+ announcementStore.setAnnouncementStatus(announcement, AnnouncementStatus.Shown)
+ }
+
+ override fun announcementsToShowFlow(): Flow> {
+ return combine(
+ announcementStore.announcementStatusFlow(Announcement.Space),
+ announcementStore.announcementStatusFlow(Announcement.NewNotificationSound),
+ ) { spaceAnnouncementStatus, newNotificationSoundStatus ->
+ buildList {
+ if (spaceAnnouncementStatus == AnnouncementStatus.Show) {
+ add(Announcement.Space)
+ }
+ if (newNotificationSoundStatus == AnnouncementStatus.Show) {
+ add(Announcement.NewNotificationSound)
+ }
+ }
+ }
+ }
+
+ private suspend fun showSpaceAnnouncement() {
+ val currentValue = announcementStore.announcementStatusFlow(Announcement.Space).first()
+ if (currentValue == AnnouncementStatus.NeverShown) {
+ announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
+ }
+ }
+
+ @Composable
+ override fun Render(modifier: Modifier) {
+ val announcementState = announcementPresenter.present()
+ Box(modifier = modifier.fillMaxSize()) {
+ AnimatedVisibility(
+ visible = announcementState.showSpaceAnnouncement,
+ enter = fadeIn(),
+ exit = fadeOut(),
+ ) {
+ val spaceAnnouncementState = spaceAnnouncementPresenter.present()
+ SpaceAnnouncementView(
+ state = spaceAnnouncementState,
+ )
+ }
+ }
+ }
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt
new file mode 100644
index 0000000000..4fbc9118bc
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.di
+
+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.announcement.impl.AnnouncementPresenter
+import io.element.android.features.announcement.impl.AnnouncementState
+import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementPresenter
+import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
+import io.element.android.libraries.architecture.Presenter
+
+@ContributesTo(AppScope::class)
+@BindingContainer
+interface AnnouncementModule {
+ @Binds
+ fun bindAnnouncementPresenter(presenter: AnnouncementPresenter): Presenter
+
+ @Binds
+ fun bindSpaceAnnouncementPresenter(presenter: SpaceAnnouncementPresenter): Presenter
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt
new file mode 100644
index 0000000000..9741608b1e
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.spaces
+
+sealed interface SpaceAnnouncementEvents {
+ data object Continue : SpaceAnnouncementEvents
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt
new file mode 100644
index 0000000000..4d1b2f3e4b
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.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.announcement.impl.spaces
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import dev.zacsweers.metro.Inject
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.impl.store.AnnouncementStatus
+import io.element.android.features.announcement.impl.store.AnnouncementStore
+import io.element.android.libraries.architecture.Presenter
+import kotlinx.coroutines.launch
+
+@Inject
+class SpaceAnnouncementPresenter(
+ private val announcementStore: AnnouncementStore,
+) : Presenter {
+ @Composable
+ override fun present(): SpaceAnnouncementState {
+ val localCoroutineScope = rememberCoroutineScope()
+
+ fun handleEvents(event: SpaceAnnouncementEvents) {
+ when (event) {
+ SpaceAnnouncementEvents.Continue -> localCoroutineScope.launch {
+ announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
+ }
+ }
+ }
+
+ return SpaceAnnouncementState(
+ eventSink = ::handleEvents
+ )
+ }
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt
new file mode 100644
index 0000000000..7628ed27ae
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.spaces
+
+data class SpaceAnnouncementState(
+ val eventSink: (SpaceAnnouncementEvents) -> Unit
+)
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt
new file mode 100644
index 0000000000..d994edf3d8
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.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.announcement.impl.spaces
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+
+open class SpaceAnnouncementStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aSpaceAnnouncementState(),
+ )
+}
+
+fun aSpaceAnnouncementState(
+ eventSink: (SpaceAnnouncementEvents) -> Unit = {},
+) = SpaceAnnouncementState(
+ eventSink = eventSink,
+)
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt
new file mode 100644
index 0000000000..2a8c5257aa
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.spaces
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.features.announcement.impl.R
+import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
+import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
+import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
+import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
+import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
+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
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.persistentListOf
+
+/**
+ * Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4593-40181
+ */
+@Composable
+fun SpaceAnnouncementView(
+ state: SpaceAnnouncementState,
+ modifier: Modifier = Modifier,
+) {
+ val eventSink = state.eventSink
+
+ fun onContinue() {
+ eventSink(SpaceAnnouncementEvents.Continue)
+ }
+
+ BackHandler(onBack = ::onContinue)
+ HeaderFooterPage(
+ modifier = modifier,
+ isScrollable = true,
+ contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp),
+ header = {
+ SpaceAnnouncementHeader()
+ },
+ content = {
+ SpaceAnnouncementContent(
+ modifier = Modifier.padding(horizontal = 8.dp),
+ )
+ },
+ footer = {
+ SpaceAnnouncementFooter(
+ onContinue = ::onContinue,
+ )
+ }
+ )
+}
+
+@Composable
+private fun SpaceAnnouncementHeader(
+ modifier: Modifier = Modifier,
+) {
+ IconTitleSubtitleMolecule(
+ modifier = modifier.padding(top = 16.dp, bottom = 16.dp),
+ title = stringResource(id = R.string.screen_space_announcement_title),
+ showBetaLabel = true,
+ subTitle = stringResource(id = R.string.screen_space_announcement_subtitle),
+ iconStyle = BigIcon.Style.Default(
+ vectorIcon = CompoundIcons.WorkspaceSolid(),
+ usePrimaryTint = true,
+ ),
+ )
+}
+
+@Composable
+private fun SpaceAnnouncementContent(
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ ) {
+ InfoListOrganism(
+ modifier = Modifier.fillMaxWidth(),
+ items = persistentListOf(
+ InfoListItem(
+ message = stringResource(id = R.string.screen_space_announcement_item1),
+ iconVector = CompoundIcons.VisibilityOn(),
+ ),
+ InfoListItem(
+ message = stringResource(id = R.string.screen_space_announcement_item2),
+ iconVector = CompoundIcons.Email(),
+ ),
+ InfoListItem(
+ message = stringResource(id = R.string.screen_space_announcement_item3),
+ iconVector = CompoundIcons.Search(),
+ ),
+ InfoListItem(
+ message = stringResource(id = R.string.screen_space_announcement_item4),
+ iconVector = CompoundIcons.Explore(),
+ ),
+ InfoListItem(
+ message = stringResource(id = R.string.screen_space_announcement_item5),
+ iconVector = CompoundIcons.Leave(),
+ ),
+ ),
+ textStyle = ElementTheme.typography.fontBodyLgMedium,
+ iconTint = ElementTheme.colors.iconSecondary,
+ iconSize = 24.dp
+ )
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
+ text = stringResource(id = R.string.screen_space_announcement_notice),
+ style = ElementTheme.typography.fontBodyMdRegular,
+ color = ElementTheme.colors.textSecondary,
+ textAlign = TextAlign.Center,
+ )
+ }
+}
+
+@Composable
+private fun SpaceAnnouncementFooter(
+ onContinue: () -> Unit,
+) {
+ ButtonColumnMolecule(
+ modifier = Modifier.padding(bottom = 8.dp)
+ ) {
+ Button(
+ text = stringResource(id = CommonStrings.action_continue),
+ onClick = onContinue,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun SpaceAnnouncementViewPreview(@PreviewParameter(SpaceAnnouncementStateProvider::class) state: SpaceAnnouncementState) = ElementPreview {
+ SpaceAnnouncementView(
+ state = state,
+ )
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStatus.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStatus.kt
new file mode 100644
index 0000000000..e275a140e1
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStatus.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.store
+
+enum class AnnouncementStatus {
+ NeverShown,
+ Show,
+ Shown,
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStore.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStore.kt
new file mode 100644
index 0000000000..3385165c52
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStore.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.store
+
+import io.element.android.features.announcement.api.Announcement
+import kotlinx.coroutines.flow.Flow
+
+interface AnnouncementStore {
+ suspend fun setAnnouncementStatus(
+ announcement: Announcement,
+ status: AnnouncementStatus,
+ )
+
+ fun announcementStatusFlow(
+ announcement: Announcement,
+ ): Flow
+
+ suspend fun reset()
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt
new file mode 100644
index 0000000000..37acf9f6b4
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.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.announcement.impl.store
+
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.intPreferencesKey
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import dev.zacsweers.metro.Inject
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+private val spaceAnnouncementKey = intPreferencesKey("spaceAnnouncement")
+private val newNotificationSoundKey = intPreferencesKey("newNotificationSound")
+
+@ContributesBinding(AppScope::class)
+@Inject
+class DefaultAnnouncementStore(
+ preferenceDataStoreFactory: PreferenceDataStoreFactory,
+) : AnnouncementStore {
+ private val store = preferenceDataStoreFactory.create("elementx_announcement")
+
+ override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStatus) {
+ val key = announcement.toKey()
+ store.edit { prefs ->
+ prefs[key] = status.ordinal
+ }
+ }
+
+ override fun announcementStatusFlow(announcement: Announcement): Flow {
+ val key = announcement.toKey()
+ // For NewNotificationSound, a migration will set it to Show on application upgrade (see AppMigration08)
+ val defaultStatus = when (announcement) {
+ Announcement.Space -> AnnouncementStatus.NeverShown
+ Announcement.NewNotificationSound -> AnnouncementStatus.Shown
+ }
+ return store.data.map { prefs ->
+ val ordinal = prefs[key] ?: defaultStatus.ordinal
+ AnnouncementStatus.entries.getOrElse(ordinal) { defaultStatus }
+ }
+ }
+
+ override suspend fun reset() {
+ store.edit { it.clear() }
+ }
+}
+
+private fun Announcement.toKey() = when (this) {
+ Announcement.Space -> spaceAnnouncementKey
+ Announcement.NewNotificationSound -> newNotificationSoundKey
+}
diff --git a/features/announcement/impl/src/main/res/values-da/translations.xml b/features/announcement/impl/src/main/res/values-da/translations.xml
new file mode 100644
index 0000000000..933ae95d6d
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-da/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Se klynger, du har oprettet eller tilmeldt dig"
+ "Acceptere eller afvise invitationer til klynger"
+ "Finde alle rum, du kan deltage i, i dine klynger"
+ "Deltage i offentlige klynger"
+ "Forlade de klynger, du har tilsluttet dig"
+ "Oprettelse og administration af klynger kommer snart."
+ "Velkommen til betaversionen af Klynger! Med denne første version kan du:"
+ "Introduktion til Klynger"
+
diff --git a/features/announcement/impl/src/main/res/values-de/translations.xml b/features/announcement/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..c38a9b5cd9
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Von dir erstellte oder beigetretene Spaces anzeigen"
+ "Einladungen zu Spaces annehmen oder ablehnen"
+ "Chats innerhalb deiner Spaces entdecken, um ihnen beizutreten"
+ "Öffentlichen Spaces beitreten"
+ "Spaces verlassen, bei denen du Mitglied bist"
+ "Das Erstellen und Verwalten von Spaces ist bald verfügbar."
+ "Willkommen bei der Beta-Version von Spaces! Mit dieser ersten Version kannst du:"
+ "Einführung in Spaces"
+
diff --git a/features/announcement/impl/src/main/res/values-fi/translations.xml b/features/announcement/impl/src/main/res/values-fi/translations.xml
new file mode 100644
index 0000000000..a5de5465ec
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-fi/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Nähdä luomasi tai liittymäsi tilat"
+ "Hyväksyä tai hylätä kutsuja tiloihin"
+ "Löytää kaikki huoneet, joihin voit liittyä tiloissasi"
+ "Liittyä julkisiin tiloihin"
+ "Poistua mistä tahansa tilasta, johon olet liittynyt"
+ "Tilojen luominen ja hallinta on tulossa pian."
+ "Tervetuloa tilojen beetaversioon! Tämän ensimmäisen version avulla voit:"
+ "Esittelyssä tilat"
+
diff --git a/features/announcement/impl/src/main/res/values-fr/translations.xml b/features/announcement/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..04a35b0fc0
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Voir les espaces que vous avez créés ou rejoints"
+ "Accepter ou refuser les invitations aux espaces"
+ "Découvrir les salons que vous pouvez joindre depuis vos espaces"
+ "Rejoindre les espaces publics"
+ "Quitter les espaces dont vous êtes membre."
+ "La création et la gestion des espaces seront bientôt disponibles."
+ "Bienvenue dans la version bêta des espaces! Avec cette première version, vous pourrez :"
+ "Ajout des espaces"
+
diff --git a/features/announcement/impl/src/main/res/values-nb/translations.xml b/features/announcement/impl/src/main/res/values-nb/translations.xml
new file mode 100644
index 0000000000..0765e557f8
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-nb/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Se områder du har opprettet eller blitt med i"
+ "Godta eller avslå invitasjoner til områder"
+ "Oppdag alle rom du kan bli med i i dine områder"
+ "Bli med i offentlige områder"
+ "Forlat områder du har blitt med i"
+ "Oppretting og administrasjon av områder kommer snart."
+ "Velkommen til betaversjonen av Områder! Med denne første versjonen kan du:"
+ "Vi introduserer Områder"
+
diff --git a/features/announcement/impl/src/main/res/values-ro/translations.xml b/features/announcement/impl/src/main/res/values-ro/translations.xml
new file mode 100644
index 0000000000..48fa06fca4
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-ro/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Vizualizați spațiile pe care le-ați creat sau la care v-ați alăturat"
+ "Acceptați sau refuzați invitațiile la spații"
+ "Descoperiți toate camerele la care vă puteți alătura în spațiile dumneavoastră."
+ "Alăturați-vă spațiilor publice"
+ "Părăsiți spațiile la care v-ați alăturat."
+ "Crearea și gestionarea spațiilor vor fi disponibile în curând."
+ "Bun venit la versiunea beta a Spațiilor! Cu această primă versiune puteți:"
+ "Vă prezentăm Spații"
+
diff --git a/features/announcement/impl/src/main/res/values-ru/translations.xml b/features/announcement/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..8b930b2ca0
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Просмотр пространств, которые вы создали или к которым присоединились"
+ "Принимать или отклонять приглашения в пространства"
+ "Откройте для себя все комнаты, к которым вы можете присоединиться в своих пространствах."
+ "Присоединиться к публичному пространству"
+ "Покинуть все пространства, к которым вы присоединились"
+ "Создание и управление пространствами станет доступно в ближайшее время."
+ "Добро пожаловать в бета-версию Spaces! В этой первой версии вы сможете:"
+ "Знакомство с пространствами"
+
diff --git a/features/announcement/impl/src/main/res/values-zh/translations.xml b/features/announcement/impl/src/main/res/values-zh/translations.xml
new file mode 100644
index 0000000000..66608eb6e5
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-zh/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "查看您创建或加入的空间"
+ "接受或拒绝空间邀请"
+
diff --git a/features/announcement/impl/src/main/res/values/localazy.xml b/features/announcement/impl/src/main/res/values/localazy.xml
new file mode 100644
index 0000000000..5e7b8a6713
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values/localazy.xml
@@ -0,0 +1,11 @@
+
+
+ "View spaces you\'ve created or joined"
+ "Accept or decline invites to spaces"
+ "Discover any rooms you can join in your spaces"
+ "Join public spaces"
+ "Leave any spaces you’ve joined"
+ "Filtering, creating and managing spaces is coming soon."
+ "Welcome to the beta version of Spaces! With this first version you can:"
+ "Introducing Spaces"
+
diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt
new file mode 100644
index 0000000000..9290773b30
--- /dev/null
+++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.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.announcement.impl
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.impl.store.AnnouncementStatus
+import io.element.android.features.announcement.impl.store.AnnouncementStore
+import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
+import io.element.android.tests.testutils.test
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class AnnouncementPresenterTest {
+ @Test
+ fun `present - initial state`() = runTest {
+ val presenter = createAnnouncementPresenter()
+ presenter.test {
+ val state = awaitItem()
+ assertThat(state.showSpaceAnnouncement).isFalse()
+ }
+ }
+
+ @Test
+ fun `present - showSpaceAnnouncement value depends on the value in the store`() = runTest {
+ val store = InMemoryAnnouncementStore()
+ val presenter = createAnnouncementPresenter(
+ announcementStore = store,
+ )
+ presenter.test {
+ val state = awaitItem()
+ assertThat(state.showSpaceAnnouncement).isFalse()
+ store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
+ val updatedState = awaitItem()
+ assertThat(updatedState.showSpaceAnnouncement).isTrue()
+ store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
+ val finalState = awaitItem()
+ assertThat(finalState.showSpaceAnnouncement).isFalse()
+ }
+ }
+}
+
+private fun createAnnouncementPresenter(
+ announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
+) = AnnouncementPresenter(
+ announcementStore = announcementStore,
+)
diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt
new file mode 100644
index 0000000000..990f506223
--- /dev/null
+++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.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.announcement.impl
+
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
+import io.element.android.features.announcement.impl.spaces.aSpaceAnnouncementState
+import io.element.android.features.announcement.impl.store.AnnouncementStatus
+import io.element.android.features.announcement.impl.store.AnnouncementStore
+import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
+import io.element.android.libraries.architecture.Presenter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class DefaultAnnouncementServiceTest {
+ @Test
+ fun `when showing Space announcement, space announcement is set to show only if it was never shown`() = runTest {
+ val announcementStore = InMemoryAnnouncementStore()
+ val sut = createDefaultAnnouncementService(
+ announcementStore = announcementStore,
+ )
+ assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
+ sut.showAnnouncement(Announcement.Space)
+ assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Show)
+ // Simulate user close the announcement
+ sut.onAnnouncementDismissed(Announcement.Space)
+ // Entering again the space tab should not change the value
+ sut.showAnnouncement(Announcement.Space)
+ assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
+ }
+
+ @Test
+ fun `when showing NewNotificationSound announcement, announcement is set to show even if it was already shown`() = runTest {
+ val announcementStore = InMemoryAnnouncementStore()
+ val sut = createDefaultAnnouncementService(
+ announcementStore = announcementStore,
+ )
+ assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.NeverShown)
+ sut.showAnnouncement(Announcement.NewNotificationSound)
+ assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.Show)
+ // Simulate user close the announcement
+ sut.onAnnouncementDismissed(Announcement.NewNotificationSound)
+ // Calling again showAnnouncement should set it back to Show
+ sut.showAnnouncement(Announcement.NewNotificationSound)
+ assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.Show)
+ }
+
+ @Test
+ fun `test announcementsToShowFlow`() = runTest {
+ val announcementStore = InMemoryAnnouncementStore()
+ val sut = createDefaultAnnouncementService(
+ announcementStore = announcementStore,
+ )
+ sut.announcementsToShowFlow().test {
+ assertThat(awaitItem()).isEmpty()
+ announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
+ assertThat(awaitItem()).containsExactly(Announcement.Space)
+ announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
+ assertThat(awaitItem()).containsExactly(Announcement.Space, Announcement.NewNotificationSound)
+ announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
+ assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound)
+ announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Shown)
+ assertThat(awaitItem()).isEmpty()
+ }
+ }
+
+ private fun createDefaultAnnouncementService(
+ announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
+ announcementPresenter: Presenter = Presenter { anAnnouncementState() },
+ spaceAnnouncementPresenter: Presenter = Presenter { aSpaceAnnouncementState() },
+ ) = DefaultAnnouncementService(
+ announcementStore = announcementStore,
+ announcementPresenter = announcementPresenter,
+ spaceAnnouncementPresenter = spaceAnnouncementPresenter,
+ )
+}
diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt
new file mode 100644
index 0000000000..2c35bccf41
--- /dev/null
+++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.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.announcement.impl.spaces
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.impl.store.AnnouncementStatus
+import io.element.android.features.announcement.impl.store.AnnouncementStore
+import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
+import io.element.android.tests.testutils.test
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class SpaceAnnouncementPresenterTest {
+ @Test
+ fun `present - when user continues, the store is updated`() = runTest {
+ val store = InMemoryAnnouncementStore()
+ val presenter = createSpaceAnnouncementPresenter(
+ announcementStore = store,
+ )
+ presenter.test {
+ assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
+ val state = awaitItem()
+ state.eventSink(SpaceAnnouncementEvents.Continue)
+ assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
+ }
+ }
+}
+
+private fun createSpaceAnnouncementPresenter(
+ announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
+) = SpaceAnnouncementPresenter(
+ announcementStore = announcementStore,
+)
diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt
new file mode 100644
index 0000000000..96b98668a4
--- /dev/null
+++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.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.announcement.impl.spaces
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.tests.testutils.EventsRecorder
+import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.pressBackKey
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SpaceAnnouncementViewTest {
+ @get:Rule val rule = createAndroidComposeRule()
+
+ @Test
+ fun `clicking on back sends a SpaceAnnouncementEvents`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setSpaceAnnouncementView(
+ aSpaceAnnouncementState(
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.pressBackKey()
+ eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue)
+ }
+
+ @Test
+ fun `clicking on Continue sends a SpaceAnnouncementEvents`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setSpaceAnnouncementView(
+ aSpaceAnnouncementState(
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.clickOn(CommonStrings.action_continue)
+ eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue)
+ }
+}
+
+private fun AndroidComposeTestRule.setSpaceAnnouncementView(
+ state: SpaceAnnouncementState,
+) {
+ setContent {
+ SpaceAnnouncementView(
+ state = state,
+ )
+ }
+}
diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt
new file mode 100644
index 0000000000..f7d438784a
--- /dev/null
+++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.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.announcement.impl.store
+
+import io.element.android.features.announcement.api.Announcement
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class InMemoryAnnouncementStore(
+ initialSpaceAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
+ initialNewNotificationSoundAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
+) : AnnouncementStore {
+ private val spaceAnnouncement = MutableStateFlow(initialSpaceAnnouncementStatus)
+ private val newNotificationSoundAnnouncement = MutableStateFlow(initialNewNotificationSoundAnnouncementStatus)
+
+ override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStatus) {
+ announcement.toMutableStateFlow().value = status
+ }
+
+ override fun announcementStatusFlow(announcement: Announcement): Flow {
+ return announcement.toMutableStateFlow().asStateFlow()
+ }
+
+ override suspend fun reset() {
+ spaceAnnouncement.value = AnnouncementStatus.NeverShown
+ newNotificationSoundAnnouncement.value = AnnouncementStatus.NeverShown
+ }
+
+ private fun Announcement.toMutableStateFlow() = when (this) {
+ Announcement.Space -> spaceAnnouncement
+ Announcement.NewNotificationSound -> newNotificationSoundAnnouncement
+ }
+}
diff --git a/features/announcement/test/build.gradle.kts b/features/announcement/test/build.gradle.kts
new file mode 100644
index 0000000000..9387dc0caf
--- /dev/null
+++ b/features/announcement/test/build.gradle.kts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+plugins {
+ id("io.element.android-compose-library")
+}
+
+android {
+ namespace = "io.element.android.features.announcement.test"
+}
+
+dependencies {
+ implementation(projects.features.announcement.api)
+ implementation(libs.coroutines.core)
+ implementation(projects.tests.testutils)
+}
diff --git a/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt b/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt
new file mode 100644
index 0000000000..74c2adf95f
--- /dev/null
+++ b/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.rageshake.test.logs
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.api.AnnouncementService
+import io.element.android.tests.testutils.lambda.lambdaError
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeAnnouncementService(
+ initialAnnouncementsToShowFlowValue: List = emptyList(),
+ val showAnnouncementResult: (Announcement) -> Unit = { lambdaError() },
+ val onAnnouncementDismissedResult: (Announcement) -> Unit = { lambdaError() },
+ val renderResult: (Modifier) -> Unit = { lambdaError() },
+) : AnnouncementService {
+ private val announcementsToShowFlowValue = MutableStateFlow(initialAnnouncementsToShowFlowValue)
+
+ override suspend fun showAnnouncement(announcement: Announcement) {
+ showAnnouncementResult(announcement)
+ }
+
+ override suspend fun onAnnouncementDismissed(announcement: Announcement) {
+ onAnnouncementDismissedResult(announcement)
+ }
+
+ override fun announcementsToShowFlow(): Flow> {
+ return announcementsToShowFlowValue.asStateFlow()
+ }
+
+ fun emitAnnouncementsToShow(value: List) {
+ announcementsToShowFlowValue.value = value
+ }
+
+ @Composable
+ override fun Render(modifier: Modifier) {
+ renderResult(modifier)
+ }
+}
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 c1c58d4607..06929093f5 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
@@ -19,7 +19,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import timber.log.Timber
import java.util.UUID
import kotlin.time.Duration.Companion.seconds
-@Inject
+@AssistedInject
class CallScreenPresenter(
@Assisted private val callType: CallType,
@Assisted private val navigator: CallScreenNavigator,
@@ -242,7 +242,7 @@ class CallScreenPresenter(
}
coroutineScope.launch {
Timber.d("Observing sync state in-call for sessionId: ${roomCallType.sessionId}")
- client.syncService().syncState
+ client.syncService.syncState
.collect { state ->
if (state != SyncState.Running) {
appForegroundStateService.updateIsInCallState(true)
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/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 697297372f..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
@@ -15,7 +15,7 @@ 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.Inject
+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
@@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import kotlinx.coroutines.flow.first
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class ChangeRolesNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 cf43874b10..632d93f0e0 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
@@ -20,7 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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
@@ -37,17 +37,15 @@ import io.element.android.libraries.matrix.ui.model.roleOf
import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
-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
-@Inject
+@AssistedInject
class ChangeRolesPresenter(
@Assisted private val role: RoomMember.Role,
private val room: JoinedRoom,
@@ -73,11 +71,11 @@ class ChangeRolesPresenter(
}
val exitState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val saveState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) }
- val usersWithRole = produceState(initialValue = persistentListOf()) {
+ val usersWithRole = produceState>(initialValue = persistentListOf()) {
room.usersWithRole(role).map { members -> members.map { it.toMatrixUser() } }
.onEach { users ->
- val previous: PersistentList = value
- value = users.toPersistentList()
+ val previous = value
+ value = users.toImmutableList()
// Users who were selected but didn't have the role, so their role change was pending
val toAdd = selectedUsers.value.filter { user -> users.none { it.userId == user.userId } && previous.none { it.userId == user.userId } }
// Users who no longer have the role
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 5055761e6e..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
@@ -17,9 +17,9 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
-import io.element.android.appnav.di.RoomComponentFactory
+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
@@ -32,11 +32,11 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class ChangeRoomMemberRolesRootNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- roomComponentFactory: RoomComponentFactory,
+ roomGraphFactory: RoomGraphFactory,
) : ParentNode(
navModel = PermanentNavModel(
navTargets = setOf(NavTarget),
@@ -54,7 +54,7 @@ class ChangeRoomMemberRolesRootNode(
private val inputs = inputs()
- override val graph = roomComponentFactory.create(inputs.joinedRoom)
+ override val graph = roomGraphFactory.create(inputs.joinedRoom)
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return createNode(
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 33c9fabe24..2e3379b98a 100644
--- a/features/changeroommemberroles/impl/src/main/res/values-nb/translations.xml
+++ b/features/changeroommemberroles/impl/src/main/res/values-nb/translations.xml
@@ -17,6 +17,7 @@
"Rediger administratorer"
"Du vil ikke kunne angre denne handlingen. Du forfremmer brukeren til å ha samme rettighetsnivå som deg."
"Legg til administrator?"
+ "Du kan ikke angre denne handlingen. Du overfører eierskapet til de valgte brukerne. Når du forlater siden, vil dette være permanent."
"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."
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 e5e22b8846..41b6acd60c 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
@@ -32,8 +32,8 @@ import io.element.android.libraries.previewutils.room.aRoomMemberList
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentMapOf
-import kotlinx.collections.immutable.toPersistentList
-import kotlinx.collections.immutable.toPersistentMap
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -103,7 +103,7 @@ class ChangeRolesPresenterTest {
// Owner - creator
aRoomMember(userId = creatorUserId, role = RoomMember.Role.Owner(isCreator = true))
)
- givenRoomMembersState(RoomMembersState.Ready(roomMemberList.toPersistentList()))
+ givenRoomMembersState(RoomMembersState.Ready(roomMemberList.toImmutableList()))
}
val presenter = createChangeRolesPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
@@ -124,7 +124,7 @@ class ChangeRolesPresenterTest {
val creatorUserId = UserId("@creator:matrix.org")
val memberList = aRoomMemberList()
.plus(aRoomMember(displayName = "CREATOR", role = RoomMember.Role.Owner(isCreator = true), userId = creatorUserId))
- .toPersistentList()
+ .toImmutableList()
givenRoomInfo(aRoomInfo(roomCreators = listOf(creatorUserId)))
givenRoomMembersState(RoomMembersState.Ready(memberList))
}
@@ -203,7 +203,7 @@ class ChangeRolesPresenterTest {
assertThat(initialResults?.moderators).hasSize(1)
assertThat(initialResults?.admins).hasSize(1)
- room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList().take(1).toPersistentList()))
+ room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList().take(1).toImmutableList()))
skipItems(1)
val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results
@@ -552,7 +552,7 @@ class ChangeRolesPresenterTest {
private fun roomPowerLevelsWithRoles(vararg pairs: Pair): RoomPowerLevels {
return RoomPowerLevels(
values = defaultRoomPowerLevelValues(),
- users = pairs.associate { (userId, role) -> userId to role.powerLevel }.toPersistentMap()
+ users = pairs.associate { (userId, role) -> userId to role.powerLevel }.toImmutableMap()
)
}
}
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
index 5f68fe5970..621af8edaf 100644
--- 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
@@ -26,7 +26,7 @@ class DefaultChangeRoomMemberRolesEntyPointTest {
ChangeRoomMemberRolesRootNode(
buildContext = buildContext,
plugins = plugins,
- roomComponentFactory = { },
+ roomGraphFactory = { },
)
}
val room = FakeJoinedRoom()
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 099098f9bc..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
@@ -17,7 +17,7 @@ import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.replace
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class CreateRoomFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 fe9ff50a5f..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
@@ -14,7 +14,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -24,7 +24,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class AddPeopleNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 0b61ce68ae..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
@@ -15,7 +15,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@@ -23,7 +23,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class ConfigureRoomNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 d63e42a2db..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
@@ -13,12 +13,12 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class AccountDeactivationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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/enterprise/api/build.gradle.kts b/features/enterprise/api/build.gradle.kts
index 9f63ab2cf1..b32f42e31f 100644
--- a/features/enterprise/api/build.gradle.kts
+++ b/features/enterprise/api/build.gradle.kts
@@ -13,7 +13,7 @@ android {
}
dependencies {
- implementation(libs.compound)
+ implementation(projects.libraries.compound)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
}
diff --git a/features/enterprise/impl-foss/build.gradle.kts b/features/enterprise/impl-foss/build.gradle.kts
index 956c0e1900..c5c194807f 100644
--- a/features/enterprise/impl-foss/build.gradle.kts
+++ b/features/enterprise/impl-foss/build.gradle.kts
@@ -18,7 +18,7 @@ android {
setupDependencyInjection()
dependencies {
- implementation(libs.compound)
+ implementation(projects.libraries.compound)
api(projects.features.enterprise.api)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
diff --git a/features/enterprise/test/build.gradle.kts b/features/enterprise/test/build.gradle.kts
index 91b76f4fa7..38cc7aaaa9 100644
--- a/features/enterprise/test/build.gradle.kts
+++ b/features/enterprise/test/build.gradle.kts
@@ -14,7 +14,7 @@ android {
dependencies {
api(projects.features.enterprise.api)
- implementation(libs.compound)
+ implementation(projects.libraries.compound)
implementation(projects.libraries.matrix.api)
implementation(projects.tests.testutils)
}
diff --git a/features/ftue/impl/build.gradle.kts b/features/ftue/impl/build.gradle.kts
index 8f940ed29a..d7d61f6d8d 100644
--- a/features/ftue/impl/build.gradle.kts
+++ b/features/ftue/impl/build.gradle.kts
@@ -34,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)
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 a4aa9f18d7..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,10 +8,7 @@
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.modality.BuildContext
@@ -20,9 +17,8 @@ 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 dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -34,15 +30,15 @@ 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.SessionScope
+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.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class FtueFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
@@ -87,7 +83,7 @@ class FtueFlowNode(
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 {
@@ -146,17 +142,3 @@ class FtueFlowNode(
BackstackView()
}
}
-
-@ContributesNode(AppScope::class)
-@Inject
-class PlaceholderNode(
- @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/notifications/NotificationsOptInNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInNode.kt
index eca83cfc06..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
@@ -14,13 +14,13 @@ 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.Inject
+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
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class NotificationsOptInNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 0dbed74b02..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
@@ -14,7 +14,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-@Inject
+@AssistedInject
class NotificationsOptInPresenter(
permissionsPresenterFactory: PermissionsPresenter.Factory,
@Assisted private val callback: NotificationsOptInNode.Callback,
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 5141924a78..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
@@ -21,7 +21,7 @@ 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 dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -37,7 +37,7 @@ import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class FtueSessionVerificationFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 a03ce8c06b..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
@@ -14,14 +14,14 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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)
-@Inject
+@AssistedInject
class ChooseSelfVerificationModeNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 e278e803c4..eb3c1330b3 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
@@ -26,7 +26,7 @@ class ChooseSelfVerificationModePresenter(
) : Presenter {
@Composable
override fun present(): ChooseSelfVerificationModeState {
- val isLastDevice by encryptionService.isLastDevice.collectAsState()
+ val hasDevicesToVerifyAgainst by encryptionService.hasDevicesToVerifyAgainst.collectAsState()
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
val canEnterRecoveryKey by remember { derivedStateOf { recoveryState == RecoveryState.INCOMPLETE } }
@@ -39,7 +39,7 @@ class ChooseSelfVerificationModePresenter(
}
return ChooseSelfVerificationModeState(
- isLastDevice = isLastDevice,
+ canUseAnotherDevice = hasDevicesToVerifyAgainst,
canEnterRecoveryKey = canEnterRecoveryKey,
directLogoutState = directLogoutState,
eventSink = ::eventHandler,
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt
index 21c37a4ae2..117768a6d2 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt
@@ -10,7 +10,7 @@ package io.element.android.features.ftue.impl.sessionverification.choosemode
import io.element.android.features.logout.api.direct.DirectLogoutState
data class ChooseSelfVerificationModeState(
- val isLastDevice: Boolean,
+ val canUseAnotherDevice: Boolean,
val canEnterRecoveryKey: Boolean,
val directLogoutState: DirectLogoutState,
val eventSink: (ChooseSelfVerificationModeEvent) -> Unit,
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt
index 574aa367c6..e053728e2c 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt
@@ -13,18 +13,18 @@ import io.element.android.features.logout.api.direct.aDirectLogoutState
class ChooseSelfVerificationModeStateProvider :
PreviewParameterProvider {
override val values = sequenceOf(
- aChooseSelfVerificationModeState(isLastDevice = true, canEnterRecoveryKey = true),
- aChooseSelfVerificationModeState(isLastDevice = true, canEnterRecoveryKey = false),
- aChooseSelfVerificationModeState(isLastDevice = false, canEnterRecoveryKey = true),
- aChooseSelfVerificationModeState(isLastDevice = false, canEnterRecoveryKey = false),
+ aChooseSelfVerificationModeState(canUseAnotherDevice = false, canEnterRecoveryKey = true),
+ aChooseSelfVerificationModeState(canUseAnotherDevice = false, canEnterRecoveryKey = false),
+ aChooseSelfVerificationModeState(canUseAnotherDevice = true, canEnterRecoveryKey = true),
+ aChooseSelfVerificationModeState(canUseAnotherDevice = true, canEnterRecoveryKey = false),
)
}
fun aChooseSelfVerificationModeState(
- isLastDevice: Boolean = false,
+ canUseAnotherDevice: Boolean = true,
canEnterRecoveryKey: Boolean = true,
) = ChooseSelfVerificationModeState(
- isLastDevice = isLastDevice,
+ canUseAnotherDevice = canUseAnotherDevice,
canEnterRecoveryKey = canEnterRecoveryKey,
directLogoutState = aDirectLogoutState(),
eventSink = {},
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt
index 74eed0115c..b07c04ac9c 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt
@@ -76,7 +76,7 @@ fun ChooseSelfVerificationModeView(
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 16.dp)
) {
- if (state.isLastDevice.not()) {
+ if (state.canUseAnotherDevice) {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_identity_use_another_device),
diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt
index 3801001ed4..3dbf5a6932 100644
--- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt
+++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt
@@ -24,15 +24,15 @@ class ChooseSessionVerificationModePresenterTest {
@Test
fun `initial state - is relayed from EncryptionService`() = runTest {
val encryptionService = FakeEncryptionService().apply {
- // Is last device
- emitIsLastDevice(true)
+ // Has device to verify against
+ emitHasDevicesToVerifyAgainst(false)
// Can enter recovery key
emitRecoveryState(RecoveryState.INCOMPLETE)
}
val presenter = createPresenter(encryptionService = encryptionService)
presenter.test {
awaitItem().run {
- assertThat(isLastDevice).isTrue()
+ assertThat(canUseAnotherDevice).isFalse()
assertThat(canEnterRecoveryKey).isTrue()
assertThat(directLogoutState.logoutAction.isUninitialized()).isTrue()
}
diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt
index b89e3e42bd..ed7d99dd19 100644
--- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt
+++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt
@@ -43,7 +43,7 @@ class ChooseSessionVerificationModeViewTest {
fun `clicking on use another device calls the callback`() {
ensureCalledOnce { callback ->
rule.setChooseSelfVerificationModeView(
- aChooseSelfVerificationModeState(isLastDevice = false),
+ aChooseSelfVerificationModeState(canUseAnotherDevice = true),
onUseAnotherDevice = callback,
)
rule.clickOn(R.string.screen_identity_use_another_device)
diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts
index b6a83775fc..9a29532ef0 100644
--- a/features/home/impl/build.gradle.kts
+++ b/features/home/impl/build.gradle.kts
@@ -45,6 +45,7 @@ dependencies {
implementation(projects.libraries.permissions.noop)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.push.api)
+ implementation(projects.features.announcement.api)
implementation(projects.features.invite.api)
implementation(projects.features.networkmonitor.api)
implementation(projects.features.logout.api)
@@ -60,6 +61,7 @@ dependencies {
api(projects.features.home.api)
testCommonDependencies(libs, true)
+ testImplementation(projects.features.announcement.test)
testImplementation(projects.features.invite.test)
testImplementation(projects.features.logout.test)
testImplementation(projects.features.networkmonitor.test)
@@ -71,6 +73,7 @@ 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)
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..9c29766067
--- /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.toImmutableList
+
+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)
+ }
+ }
+ }
+ }
+ .toImmutableList()
+ }
+}
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 d8ecf7016f..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
@@ -26,7 +26,7 @@ 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.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
@@ -56,7 +56,7 @@ import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class HomeFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 90565de292..e3ca9612d1 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.announcement.api.Announcement
+import io.element.android.features.announcement.api.AnnouncementService
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
@@ -29,6 +32,10 @@ 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 io.element.android.libraries.sessionstorage.api.SessionStore
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
@Inject
class HomePresenter(
@@ -41,10 +48,22 @@ class HomePresenter(
private val logoutPresenter: Presenter,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val featureFlagService: FeatureFlagService,
+ private val sessionStore: SessionStore,
+ private val announcementService: AnnouncementService,
) : 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()
@@ -68,9 +87,15 @@ class HomePresenter(
fun handleEvents(event: HomeEvents) {
when (event) {
- is HomeEvents.SelectHomeNavigationBarItem -> {
+ is HomeEvents.SelectHomeNavigationBarItem -> coroutineState.launch {
+ if (event.item == HomeNavigationBarItem.Spaces) {
+ announcementService.showAnnouncement(Announcement.Space)
+ }
currentHomeNavigationBarItemOrdinal = event.item.ordinal
}
+ is HomeEvents.SwitchToAccount -> coroutineState.launch {
+ sessionStore.setLatestSession(event.sessionId.value)
+ }
}
}
@@ -82,7 +107,7 @@ class HomePresenter(
}
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
return HomeState(
- matrixUser = matrixUser.value,
+ currentUserAndNeighbors = currentUserAndNeighbors,
showAvatarIndicator = showAvatarIndicator,
hasNetworkConnection = isOnline,
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
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 c4fe0ce0fe..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
@@ -13,10 +13,15 @@ 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,
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 59c8c3c500..c5bb339661 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
@@ -21,6 +21,7 @@ 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.toImmutableList
open class HomeStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -50,6 +51,7 @@ 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,
@@ -61,7 +63,7 @@ internal fun aHomeState(
directLogoutState: DirectLogoutState = aDirectLogoutState(),
eventSink: (HomeEvents) -> Unit = {}
) = HomeState(
- matrixUser = matrixUser,
+ currentUserAndNeighbors = currentUserAndNeighbors.toImmutableList(),
showAvatarIndicator = showAvatarIndicator,
hasNetworkConnection = hasNetworkConnection,
snackbarMessage = snackbarMessage,
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 37727712fb..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
@@ -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,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/NewNotificationSoundBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/NewNotificationSoundBanner.kt
new file mode 100644
index 0000000000..f7516e41e8
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/NewNotificationSoundBanner.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.home.impl.components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import io.element.android.features.home.impl.R
+import io.element.android.libraries.designsystem.components.Announcement
+import io.element.android.libraries.designsystem.components.AnnouncementType
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.ui.strings.CommonStrings
+
+@Composable
+internal fun NewNotificationSoundBanner(
+ onDismissClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Announcement(
+ modifier = modifier.roomListBannerPadding(),
+ title = stringResource(R.string.banner_new_sound_title),
+ description = stringResource(R.string.banner_new_sound_message),
+ type = AnnouncementType.Actionable(
+ actionText = stringResource(CommonStrings.action_ok),
+ onActionClick = onDismissClick,
+ onDismissClick = onDismissClick,
+ ),
+ )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun NewNotificationSoundBannerPreview() = ElementPreview {
+ NewNotificationSoundBanner(
+ onDismissClick = {},
+ )
+}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt
index 2845a79b8e..34d036b204 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt
@@ -251,6 +251,12 @@ private fun RoomsViewList(
item {
BatteryOptimizationBanner(state = state.batteryOptimizationState)
}
+ } else if (state.showNewNotificationSoundBanner) {
+ item {
+ NewNotificationSoundBanner(
+ onDismissClick = { updatedEventSink(RoomListEvents.DismissNewNotificationSoundBanner) },
+ )
+ }
}
}
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..abd6e7892d 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.toImmutableList
@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).toImmutableList(),
+ 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 3036865eea..c5e1798baa 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
@@ -189,10 +189,14 @@ private fun RoomSummaryScaffoldRow(
) {
Avatar(
avatarData = room.avatarData,
- avatarType = AvatarType.Room(
- heroes = room.heroes,
- isTombstoned = room.isTombstoned,
- ),
+ avatarType = if (room.isSpace) {
+ AvatarType.Space(isTombstoned = room.isTombstoned)
+ } else {
+ AvatarType.Room(
+ heroes = room.heroes,
+ isTombstoned = room.isTombstoned,
+ )
+ },
hideImage = hideAvatarImage,
)
Spacer(modifier = Modifier.width(16.dp))
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 b6f908fd5d..ffd6f640ac 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
@@ -69,6 +69,7 @@ class RoomListRoomSummaryFactory(
user.getAvatarData(size = AvatarSize.RoomListItem)
}.toImmutableList(),
isTombstoned = roomInfo.successorRoom != null,
+ isSpace = roomInfo.isSpace,
)
}
}
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 07d2de96a1..08c34d60ff 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
@@ -14,7 +14,7 @@ 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.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.map
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
@@ -23,7 +23,7 @@ class RoomListFiltersPresenter(
private val roomListService: RoomListService,
private val filterSelectionStrategy: FilterSelectionStrategy,
) : Presenter {
- private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toPersistentList()
+ private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toImmutableList()
@Composable
override fun present(): RoomListFiltersState {
@@ -41,7 +41,7 @@ class RoomListFiltersPresenter(
val filters by produceState(initialValue = initialFilters) {
filterSelectionStrategy.filterSelectionStates
.map { filters ->
- value = filters.toPersistentList()
+ value = filters.toImmutableList()
filters.mapNotNull { filterState ->
if (!filterState.isSelected) {
return@mapNotNull null
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersState.kt
index bb4146ce84..215641c5b2 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersState.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersState.kt
@@ -9,7 +9,7 @@ package io.element.android.features.home.impl.filters
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
data class RoomListFiltersState(
val filterSelectionStates: ImmutableList,
@@ -21,6 +21,6 @@ data class RoomListFiltersState(
return filterSelectionStates
.filter { it.isSelected }
.map { it.filter }
- .toPersistentList()
+ .toImmutableList()
}
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt
index 3f166a66b4..8af359d3e5 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt
@@ -38,6 +38,7 @@ data class RoomListRoomSummary(
val inviteSender: InviteSender?,
val isTombstoned: Boolean,
val heroes: ImmutableList,
+ val isSpace: Boolean,
) {
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt
index f06e5a1a27..1e763843af 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt
@@ -102,6 +102,15 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider = emptyList(),
isTombstoned: Boolean = false,
+ isSpace: Boolean = false,
) = RoomListRoomSummary(
id = id,
roomId = RoomId(id),
@@ -172,4 +182,5 @@ internal fun aRoomListRoomSummary(
canonicalAlias = canonicalAlias,
heroes = heroes.toImmutableList(),
isTombstoned = isTombstoned,
+ isSpace = isSpace
)
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt
index a421c239cb..86ab5cd39c 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt
@@ -16,7 +16,7 @@ import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toPersistentSet
+import kotlinx.collections.immutable.toImmutableSet
open class RoomListContentStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -26,21 +26,26 @@ open class RoomListContentStateProvider : PreviewParameterProvider = aRoomListRoomSummaryList(),
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(),
seenRoomInvites: Set = emptySet(),
) = RoomListContentState.Rooms(
securityBannerState = securityBannerState,
+ showNewNotificationSoundBanner = showNewNotificationSoundBanner,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
batteryOptimizationState = batteryOptimizationState,
summaries = summaries,
- seenRoomInvites = seenRoomInvites.toPersistentSet(),
+ seenRoomInvites = seenRoomInvites.toImmutableSet(),
)
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvents.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvents.kt
index 02df2cac35..52da613be7 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvents.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvents.kt
@@ -14,6 +14,7 @@ sealed interface RoomListEvents {
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
data object DismissRequestVerificationPrompt : RoomListEvents
data object DismissBanner : RoomListEvents
+ data object DismissNewNotificationSoundBanner : RoomListEvents
data object ToggleSearchResults : RoomListEvents
data class ShowContextMenu(val roomSummary: RoomListRoomSummary) : RoomListEvents
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 b8e299c5e9..65d4eb297d 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
@@ -24,6 +24,8 @@ 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.announcement.api.Announcement
+import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.datasource.RoomListDataSource
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.search.RoomListSearchEvents
@@ -39,7 +41,6 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.timeline.ReceiptType
@@ -50,8 +51,8 @@ import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
-import kotlinx.collections.immutable.toPersistentList
-import kotlinx.collections.immutable.toPersistentSet
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
@@ -83,8 +84,9 @@ class RoomListPresenter(
private val notificationCleaner: NotificationCleaner,
private val appPreferencesStore: AppPreferencesStore,
private val seenInvitesStore: SeenInvitesStore,
+ private val announcementService: AnnouncementService,
) : Presenter {
- private val encryptionService: EncryptionService = client.encryptionService()
+ private val encryptionService = client.encryptionService
@Composable
override fun present(): RoomListState {
@@ -99,6 +101,11 @@ class RoomListPresenter(
}
var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
+ val showNewNotificationSoundBanner by remember {
+ announcementService.announcementsToShowFlow().map { announcements ->
+ announcements.contains(Announcement.NewNotificationSound)
+ }
+ }.collectAsState(false)
// Avatar indicator
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
@@ -113,6 +120,9 @@ class RoomListPresenter(
}
RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true
RoomListEvents.DismissBanner -> securityBannerDismissed = true
+ RoomListEvents.DismissNewNotificationSoundBanner -> coroutineScope.launch {
+ announcementService.onAnnouncementDismissed(Announcement.NewNotificationSound)
+ }
RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
is RoomListEvents.ShowContextMenu -> {
coroutineScope.showContextMenu(event, contextMenu)
@@ -142,7 +152,10 @@ class RoomListPresenter(
}
}
- val contentState = roomListContentState(securityBannerDismissed)
+ val contentState = roomListContentState(
+ securityBannerDismissed,
+ showNewNotificationSoundBanner,
+ )
val canReportRoom by produceState(false) { value = client.canReportRoom() }
@@ -198,6 +211,7 @@ class RoomListPresenter(
@Composable
private fun roomListContentState(
securityBannerDismissed: Boolean,
+ showNewNotificationSoundBanner: Boolean,
): RoomListContentState {
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
@@ -216,15 +230,18 @@ class RoomListPresenter(
val seenRoomInvites by remember { seenInvitesStore.seenRoomIds() }.collectAsState(emptySet())
val securityBannerState by rememberSecurityBannerState(securityBannerDismissed)
return when {
- showEmpty -> RoomListContentState.Empty(securityBannerState = securityBannerState)
+ showEmpty -> RoomListContentState.Empty(
+ securityBannerState = securityBannerState,
+ )
showSkeleton -> RoomListContentState.Skeleton(count = 16)
else -> {
RoomListContentState.Rooms(
securityBannerState = securityBannerState,
+ showNewNotificationSoundBanner = showNewNotificationSoundBanner,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
batteryOptimizationState = batteryOptimizationPresenter.present(),
- summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList(),
- seenRoomInvites = seenRoomInvites.toPersistentSet(),
+ summaries = roomSummaries.dataOrNull().orEmpty().toImmutableList(),
+ seenRoomInvites = seenRoomInvites.toImmutableSet(),
)
}
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt
index 4a301f0897..80cd6394e8 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt
@@ -69,6 +69,7 @@ sealed interface RoomListContentState {
val securityBannerState: SecurityBannerState,
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
val batteryOptimizationState: BatteryOptimizationState,
+ val showNewNotificationSoundBanner: Boolean,
val summaries: ImmutableList,
val seenRoomInvites: ImmutableSet,
) : RoomListContentState
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..faa811b920 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,18 +16,16 @@ 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
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
open class RoomListStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -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(
@@ -134,5 +122,5 @@ internal fun generateRoomListRoomSummaryList(
avatarData = AvatarData("!id$index", "${(65 + index % 26).toChar()}", size = AvatarSize.RoomListItem),
id = "!roomId$index:domain",
)
- }.toPersistentList()
+ }.toImmutableList()
}
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 4cb4104637..ef853bd1cf 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
@@ -15,8 +15,8 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
-import kotlinx.collections.immutable.PersistentList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
@@ -36,11 +36,11 @@ class RoomListSearchDataSource(
source = RoomList.Source.All,
)
- val roomSummaries: Flow> = roomList.filteredSummaries
+ val roomSummaries: Flow> = roomList.filteredSummaries
.map { roomSummaries ->
roomSummaries
.map(roomSummaryFactory::create)
- .toPersistentList()
+ .toImmutableList()
}
.flowOn(coroutineDispatchers.computation)
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
index dea6defc0a..8c3e2962ac 100644
--- 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
@@ -17,7 +17,7 @@ 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.collections.immutable.toImmutableSet
import kotlinx.coroutines.flow.map
@Inject
@@ -30,7 +30,7 @@ class HomeSpacesPresenter(
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
val spaceRooms by client.spaceService.spaceRoomsFlow.collectAsState(emptyList())
val seenSpaceInvites by remember {
- seenInvitesStore.seenRoomIds().map { it.toPersistentSet() }
+ seenInvitesStore.seenRoomIds().map { it.toImmutableSet() }
}.collectAsState(persistentSetOf())
fun handleEvents(event: HomeSpacesEvents) {
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
index 8b07b9f526..b18e732b42 100644
--- 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
@@ -33,22 +33,17 @@ fun HomeSpacesView(
when (space) {
CurrentSpace.Root -> {
item {
- SpaceHeaderRootView(
- numberOfSpaces = state.spaceRooms.size,
- // TODO
- numberOfRooms = 0,
- )
+ SpaceHeaderRootView(numberOfSpaces = state.spaceRooms.size)
}
}
is CurrentSpace.Space -> item {
SpaceHeaderView(
avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader),
- name = space.spaceRoom.name,
+ name = space.spaceRoom.displayName,
topic = space.spaceRoom.topic,
- joinRule = space.spaceRoom.joinRule,
+ visibility = space.spaceRoom.visibility,
heroes = space.spaceRoom.heroes.toImmutableList(),
numberOfMembers = space.spaceRoom.numJoinedMembers,
- numberOfRooms = space.spaceRoom.childrenCount,
)
}
}
@@ -64,7 +59,7 @@ fun HomeSpacesView(
},
onLongClick = {
// TODO
- }
+ },
)
}
}
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
index 474e08293a..b1b3ac1950 100644
--- 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
@@ -30,7 +30,6 @@ class SpaceRoomProvider : PreviewParameterProvider {
roomId = RoomId("!spaceId1:example.com"),
),
aSpaceRoom(
- name = null,
numJoinedMembers = 5,
childrenCount = 10,
worldReadable = true,
@@ -38,14 +37,5 @@ class SpaceRoomProvider : PreviewParameterProvider {
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-da/translations.xml b/features/home/impl/src/main/res/values-da/translations.xml
index b5cc2ef7cb..b686fb6e03 100644
--- a/features/home/impl/src/main/res/values-da/translations.xml
+++ b/features/home/impl/src/main/res/values-da/translations.xml
@@ -3,6 +3,8 @@
"Deaktiver batterioptimering for denne app for at sikre, at alle notifikationer dukker op."
"Deaktivér optimering"
"Modtager du ikke notifikationer?"
+ "Dit notifikationsping er blevet opdateret – tydeligere, hurtigere og mindre forstyrrende."
+ "Vi har opdateret dine lyde"
"Gendan din kryptografiske identitet og meddelelseshistorik med en gendannelsesnøgle, hvis du har mistet alle dine eksisterende enheder."
"Opsæt gendannelse"
"Konfigurer gendannelse for at beskytte din konto"
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 0596dc45e4..2502df5fc1 100644
--- a/features/home/impl/src/main/res/values-de/translations.xml
+++ b/features/home/impl/src/main/res/values-de/translations.xml
@@ -3,6 +3,8 @@
"Deaktiviere die Batterieoptimierung für diese App, um sicherzustellen, dass alle Benachrichtigungen empfangen werden."
"Optimierung deaktivieren"
"Kommen die Benachrichtigungen nicht an?"
+ "Dein Benachrichtigungs-Ping wurde aktualisiert – klarer, schneller und weniger störend."
+ "Wir haben deine Sounds aktualisiert"
"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"
diff --git a/features/home/impl/src/main/res/values-eo/translations.xml b/features/home/impl/src/main/res/values-eo/translations.xml
index 0dc9b8e816..46986026aa 100644
--- a/features/home/impl/src/main/res/values-eo/translations.xml
+++ b/features/home/impl/src/main/res/values-eo/translations.xml
@@ -7,5 +7,5 @@
"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 connected device to access your encrypted messages."
+ "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-fi/translations.xml b/features/home/impl/src/main/res/values-fi/translations.xml
index 31b5ff0a1a..fa7150b546 100644
--- a/features/home/impl/src/main/res/values-fi/translations.xml
+++ b/features/home/impl/src/main/res/values-fi/translations.xml
@@ -3,6 +3,8 @@
"Ota tämän sovelluksen akunkäytön optimointi pois käytöstä varmistaaksesi, että kaikki ilmoitukset tulevat perille."
"Ota optimointi pois käytöstä"
"Eikö ilmoitukset tule perille?"
+ "Ilmoitusääni on päivitetty — selkeämpi, nopeampi ja vähemmän häiritsevä."
+ "Olemme päivittäneet äänesi"
"Palauta kryptografinen identiteettisi ja viestihistoriasi palautusavaimella, mikäli menetät pääsyn kaikkiin laitteisiisi."
"Ota palautus käyttöön"
"Ota palautus käyttöön tilisi suojaamiseksi"
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 11f842916f..d54e8fb943 100644
--- a/features/home/impl/src/main/res/values-fr/translations.xml
+++ b/features/home/impl/src/main/res/values-fr/translations.xml
@@ -3,6 +3,8 @@
"Désactivez l’optimisation de la batterie pour cette application afin de vous assurer que toutes les notifications sont reçues."
"Désactiver l’optimisation"
"Ils vous manque des notifications?"
+ "Le son des notifications a été modifié: plus clair, plus court et moins perturbateur."
+ "Nous avons rafraîchi les sons"
"Générez une nouvelle clé de récupération qui peut être utilisée pour restaurer l’historique de vos messages chiffrés au cas où vous perdriez l’accès à vos appareils."
"Configurer la sauvegarde"
"Configurer la récupération"
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-nb/translations.xml b/features/home/impl/src/main/res/values-nb/translations.xml
index 198bb7112d..42216442cb 100644
--- a/features/home/impl/src/main/res/values-nb/translations.xml
+++ b/features/home/impl/src/main/res/values-nb/translations.xml
@@ -3,6 +3,8 @@
"Deaktiver batterioptimalisering for denne appen for å sikre at alle varsler mottas."
"Deaktiver optimalisering"
"Kommer ikke varslene frem?"
+ "Varslingssignalet ditt er oppdatert – tydeligere, raskere og mindre forstyrrende."
+ "Vi har oppdatert lydene dine"
"Gjenopprett din kryptografiske identitet og meldingshistorikk med en gjenopprettingsnøkkel hvis du har mistet alle dine brukte enheter."
"Konfigurer gjenoppretting"
"Konfigurer gjenoppretting for å beskytte kontoen din"
@@ -33,6 +35,7 @@ Inntil videre kan du velge bort filtre for å se de andre chattene dine"
"Invitasjoner"
"Du har ingen ventende invitasjoner."
"Lav prioritet"
+ "Du har ingen lavprioriterte chatter ennå"
"Du kan velge bort filtre for å se de andre chattene dine"
"Du har ikke chatter for dette utvalget"
"Personer"
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 36a32bf089..e4a80b4fb7 100644
--- a/features/home/impl/src/main/res/values-ro/translations.xml
+++ b/features/home/impl/src/main/res/values-ro/translations.xml
@@ -3,6 +3,8 @@
"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?"
+ "Sunetul pentru notificări a fost actualizat — mai clar, mai rapid și mai puțin perturbatoar."
+ "Am reîmprospătat sunetele"
"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"
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 5ff6a3c714..0f77d56db4 100644
--- a/features/home/impl/src/main/res/values-ru/translations.xml
+++ b/features/home/impl/src/main/res/values-ru/translations.xml
@@ -3,6 +3,8 @@
"Выключите оптимизацию расхода батареи, чтобы убедиться, что все уведомления будут поступать."
"Выключить оптимизацию"
"Уведомления не поступают?"
+ "Ваши уведомления были обновлены — теперь они понятнее, быстрее и менее отвлекающие."
+ "Мы обновили ваши звуки"
"Создайте новый ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам."
"Настроить восстановление"
"Для защиты вашего аккаунта рекомендуется настроить восстановление"
@@ -13,6 +15,7 @@
"Чтобы больше не пропускать важные звонки, разрешите приложению показывать полноэкранные уведомления на заблокированном экране телефона."
"Улучшите качество звонков"
"Все чаты"
+ "Пространства"
"Вы уверены, что хотите отклонить приглашение в %1$s?"
"Отклонить приглашение"
"Вы уверены, что хотите отказаться от личного общения с %1$s?"
@@ -32,6 +35,7 @@
"Приглашения"
"У вас нет отложенных приглашений."
"Низкий приоритет"
+ "У вас пока нет чатов с низким приоритетом."
"Вы можете убрать фильтры, чтобы увидеть другие ваши чаты."
"У вас нет чатов для этой подборки"
"Пользователи"
diff --git a/features/home/impl/src/main/res/values/localazy.xml b/features/home/impl/src/main/res/values/localazy.xml
index e6e09b5e47..80a263a179 100644
--- a/features/home/impl/src/main/res/values/localazy.xml
+++ b/features/home/impl/src/main/res/values/localazy.xml
@@ -3,6 +3,8 @@
"Disable battery optimisation for this app, to make sure all notifications are received."
"Disable optimisation"
"Notifications not arriving?"
+ "Your notification ping has been updated—clearer, quicker, and less disruptive."
+ "We’ve refreshed your sounds"
"Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."
"Set up recovery"
"Set up recovery to protect your account"
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/HomePresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt
index a84dbd6309..938e0720d7 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
@@ -11,11 +11,14 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.api.AnnouncementService
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.features.rageshake.test.logs.FakeAnnouncementService
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlagService
@@ -31,9 +34,15 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
+import io.element.android.libraries.matrix.test.core.aBuildMeta
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.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
@@ -44,6 +53,8 @@ class HomePresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
+ private val isSpaceEnabled = FeatureFlags.Space.defaultValue(aBuildMeta())
+
@Test
fun `present - should start with no user and then load user with success`() = runTest {
val matrixClient = FakeMatrixClient(
@@ -54,20 +65,33 @@ 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 {
+ if (isSpaceEnabled) skipItems(1)
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()
+ assertThat(withUserState.isSpaceFeatureEnabled).isEqualTo(isSpaceEnabled)
+ assertThat(withUserState.showNavigationBar).isEqualTo(isSpaceEnabled)
}
}
@@ -75,6 +99,9 @@ class HomePresenterTest {
fun `present - can report bug`() = runTest {
val presenter = createHomePresenter(
rageshakeFeatureAvailability = { flowOf(true) },
+ sessionStore = InMemorySessionStore(
+ updateUserProfileResult = { _, _, _ -> },
+ ),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -92,6 +119,9 @@ class HomePresenterTest {
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.Space.key to true),
),
+ sessionStore = InMemorySessionStore(
+ updateUserProfileResult = { _, _, _ -> },
+ ),
)
presenter.test {
skipItems(1)
@@ -105,10 +135,14 @@ class HomePresenterTest {
val indicatorService = FakeIndicatorService()
val presenter = createHomePresenter(
indicatorService = indicatorService,
+ sessionStore = InMemorySessionStore(
+ updateUserProfileResult = { _, _, _ -> },
+ ),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
+ if (isSpaceEnabled) skipItems(1)
val initialState = awaitItem()
assertThat(initialState.showAvatarIndicator).isFalse()
indicatorService.setShowRoomListTopBarIndicator(true)
@@ -124,27 +158,44 @@ 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 {
+ if (isSpaceEnabled) skipItems(1)
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 showAnnouncementResult = lambdaRecorder { }
+ val presenter = createHomePresenter(
+ sessionStore = InMemorySessionStore(
+ updateUserProfileResult = { _, _, _ -> },
+ ),
+ announcementService = FakeAnnouncementService(
+ showAnnouncementResult = showAnnouncementResult,
+ )
+ )
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
+ if (isSpaceEnabled) skipItems(1)
val initialState = awaitItem()
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
initialState.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
val finalState = awaitItem()
assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
+ showAnnouncementResult.assertions().isCalledOnce()
+ .with(value(Announcement.Space))
}
}
@@ -152,10 +203,16 @@ class HomePresenterTest {
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,
+ announcementService = FakeAnnouncementService(
+ showAnnouncementResult = {},
+ )
)
presenter.test {
skipItems(1)
@@ -185,6 +242,8 @@ internal fun createHomePresenter(
indicatorService: IndicatorService = FakeIndicatorService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
homeSpacesPresenter: Presenter = Presenter { aHomeSpacesState() },
+ sessionStore: SessionStore = InMemorySessionStore(),
+ announcementService: AnnouncementService = FakeAnnouncementService(),
) = HomePresenter(
client = client,
syncService = syncService,
@@ -195,4 +254,6 @@ internal fun createHomePresenter(
homeSpacesPresenter = homeSpacesPresenter,
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
featureFlagService = featureFlagService,
+ sessionStore = sessionStore,
+ announcementService = announcementService,
)
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt
index 55b3f1ffee..b62c19fc7b 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt
@@ -13,7 +13,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import org.junit.Test
class RoomListBaseRoomSummaryTest {
@@ -85,6 +85,7 @@ internal fun createRoomListRoomSummary(
heroes: List = emptyList(),
timestamp: String? = null,
isTombstoned: Boolean = false,
+ isSpace: Boolean = false,
) = RoomListRoomSummary(
id = A_ROOM_ID.value,
roomId = A_ROOM_ID,
@@ -104,6 +105,7 @@ internal fun createRoomListRoomSummary(
canonicalAlias = null,
inviteSender = null,
isDm = false,
- heroes = heroes.toPersistentList(),
+ heroes = heroes.toImmutableList(),
isTombstoned = isTombstoned,
+ isSpace = isSpace
)
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..bef312cdea 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
@@ -12,6 +12,8 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Interaction
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.FakeDateTimeObserver
import io.element.android.features.home.impl.datasource.RoomListDataSource
import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory
@@ -24,9 +26,11 @@ 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
+import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
@@ -74,6 +78,7 @@ import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
@@ -592,6 +597,42 @@ class RoomListPresenterTest {
}
}
+ @Test
+ fun `present - notification sound banner`() = runTest {
+ val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List -> }
+ val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
+ val matrixClient = FakeMatrixClient(
+ roomListService = roomListService,
+ )
+ val roomSummary = aRoomSummary(
+ currentUserMembership = CurrentUserMembership.INVITED
+ )
+ roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
+ roomListService.postAllRooms(listOf(roomSummary))
+ val onAnnouncementDismissedResult = lambdaRecorder { }
+ val announcementService = FakeAnnouncementService(
+ onAnnouncementDismissedResult = onAnnouncementDismissedResult,
+ )
+ val presenter = createRoomListPresenter(
+ client = matrixClient,
+ announcementService = announcementService,
+ )
+ presenter.test {
+ assertThat(announcementService.announcementsToShowFlow().first()).isEmpty()
+ skipItems(1)
+ val state = awaitItem()
+ assertThat(state.contentAsRooms().showNewNotificationSoundBanner).isFalse()
+ announcementService.emitAnnouncementsToShow(listOf(Announcement.NewNotificationSound))
+ assertThat(awaitItem().contentAsRooms().showNewNotificationSoundBanner).isTrue()
+ state.eventSink(RoomListEvents.DismissNewNotificationSoundBanner)
+ onAnnouncementDismissedResult.assertions().isCalledOnce()
+ .with(value(Announcement.NewNotificationSound))
+ // Simulate service updating the value
+ announcementService.emitAnnouncementsToShow(emptyList())
+ assertThat(awaitItem().contentAsRooms().showNewNotificationSoundBanner).isFalse()
+ }
+ }
+
private fun TestScope.createRoomListPresenter(
client: MatrixClient = FakeMatrixClient(),
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
@@ -605,6 +646,7 @@ class RoomListPresenterTest {
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
+ announcementService: AnnouncementService = FakeAnnouncementService(),
) = RoomListPresenter(
client = client,
leaveRoomPresenter = { leaveRoomState },
@@ -615,7 +657,7 @@ class RoomListPresenterTest {
roomLastMessageFormatter = roomLastMessageFormatter,
),
coroutineDispatchers = testCoroutineDispatchers(),
- notificationSettingsService = client.notificationSettingsService(),
+ notificationSettingsService = client.notificationSettingsService,
sessionCoroutineScope = backgroundScope,
dateTimeObserver = FakeDateTimeObserver(),
),
@@ -629,5 +671,6 @@ class RoomListPresenterTest {
notificationCleaner = notificationCleaner,
appPreferencesStore = appPreferencesStore,
seenInvitesStore = seenInvitesStore,
+ announcementService = announcementService,
)
}
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 fa296edc1c..9f826ac228 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
@@ -41,7 +41,7 @@ fun RoomInfo.toInviteData(): InviteData {
fun SpaceRoom.toInviteData(): InviteData {
return InviteData(
roomId = roomId,
- roomName = name ?: roomId.value,
+ roomName = displayName,
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/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/declineandblock/DeclineAndBlockNode.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt
index 7264ee4636..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
@@ -13,7 +13,7 @@ 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.Inject
+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
@@ -21,7 +21,7 @@ import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class DeclineAndBlockNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 f9639f69fe..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
@@ -17,7 +17,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 +28,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-@Inject
+@AssistedInject
class DeclineAndBlockPresenter(
@Assisted private val inviteData: InviteData,
private val declineInvite: DeclineInvite,
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..e089814fe9 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,9 @@
"Блокиране на потребителя"
"Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?"
"Отказване на покана"
+ "Сигурни ли сте, че искате да откажете този личен чат с %1$s?"
+ "Отказване на чат"
"Няма покани"
"%1$s (%2$s) ви покани"
+ "Отхвърляне и блокиране"
diff --git a/features/invite/impl/src/main/res/values-cs/translations.xml b/features/invite/impl/src/main/res/values-cs/translations.xml
index f576269343..28da3dc4b5 100644
--- a/features/invite/impl/src/main/res/values-cs/translations.xml
+++ b/features/invite/impl/src/main/res/values-cs/translations.xml
@@ -3,7 +3,7 @@
"Od tohoto uživatele neuvidíte žádné zprávy ani pozvánky do místnosti"
"Zablokovat uživatele"
"Nahlaste tuto místnost svému poskytovateli účtu."
- "Popište důvod nahlášení…"
+ "Popište důvod…"
"Odmítnout a zablokovat"
"Opravdu chcete odmítnout pozvánku do %1$s?"
"Odmítnout pozvání"
diff --git a/features/invite/impl/src/main/res/values-cy/translations.xml b/features/invite/impl/src/main/res/values-cy/translations.xml
index a84fa2397a..e32337a162 100644
--- a/features/invite/impl/src/main/res/values-cy/translations.xml
+++ b/features/invite/impl/src/main/res/values-cy/translations.xml
@@ -3,7 +3,7 @@
"Fyddwch chi ddim yn gweld unrhyw negeseuon neu wahoddiadau ystafell gan y defnyddiwr hwn"
"Rhwystro defnyddiwr"
"Adrodd am yr ystafell hon i ddarparwr eich cyfrif."
- "Disgrifiwch y rheswm dros adrodd…"
+ "Disgrifiwch y rheswm…"
"Gwrthod a rhwystro"
"Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â %1$s?"
"Gwrthod y gwahoddiad"
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 bb118a99e8..7b6ca2eb61 100644
--- a/features/invite/impl/src/main/res/values-de/translations.xml
+++ b/features/invite/impl/src/main/res/values-de/translations.xml
@@ -3,7 +3,7 @@
"Du wirst keine Nachrichten oder Chat-Einladungen von diesem Nutzer sehen."
"Nutzer blockieren"
"Melde diesen Chat deinem Konto-Anbieter."
- "Nenne den Grund für die Meldung…"
+ "Beschreibe den Grund für die Meldung…"
"Ablehnen und blockieren"
"Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?"
"Einladung ablehnen"
diff --git a/features/invite/impl/src/main/res/values-et/translations.xml b/features/invite/impl/src/main/res/values-et/translations.xml
index 91906fa877..1bc1e93b59 100644
--- a/features/invite/impl/src/main/res/values-et/translations.xml
+++ b/features/invite/impl/src/main/res/values-et/translations.xml
@@ -3,7 +3,7 @@
"Sa ei näe enam selle kasutaja saadetud sõnumeid ja jututubade kutseid"
"Blokeeri kasutaja"
"Teata sellest jututoast oma teenusepakkujale."
- "Kirjelda teatamise põhjust…"
+ "Kirjelda põhjust…"
"Keeldu ja blokeeri"
"Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?"
"Lükka kutse tagasi"
diff --git a/features/invite/impl/src/main/res/values-eu/translations.xml b/features/invite/impl/src/main/res/values-eu/translations.xml
index 49a545c4c9..289a8fa418 100644
--- a/features/invite/impl/src/main/res/values-eu/translations.xml
+++ b/features/invite/impl/src/main/res/values-eu/translations.xml
@@ -9,5 +9,5 @@
"Ez dago gonbidapenik"
"%1$s(e)k (%2$s) gonbidatu zaitu"
"Eman gonbidapenari ezetza eta blokeatu"
- "Eman ezetza eta blokeatu"
+ "Baztertu eta blokeatu"
diff --git a/features/invite/impl/src/main/res/values-fi/translations.xml b/features/invite/impl/src/main/res/values-fi/translations.xml
index 47ad368d73..215e2ee31b 100644
--- a/features/invite/impl/src/main/res/values-fi/translations.xml
+++ b/features/invite/impl/src/main/res/values-fi/translations.xml
@@ -3,7 +3,7 @@
"Et tule näkemään viestejä tai kutsuja tältä käyttäjältä"
"Estä käyttäjä"
"Ilmoita tästä huoneesta palveluntarjoajallesi."
- "Kerro syy ilmoittamiseen…"
+ "Kuvaile syytä…"
"Hylkää ja estä"
"Haluatko varmasti hylätä kutsun liittyä %1$s -huoneeseen?"
"Hylkää kutsu"
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..e75d2961ed 100644
--- a/features/invite/impl/src/main/res/values-hu/translations.xml
+++ b/features/invite/impl/src/main/res/values-hu/translations.xml
@@ -3,8 +3,8 @@
"Ettől a felhasználótól nem fog többé üzeneteket vagy meghívásokat látni."
"Felhasználó letiltása"
"A szoba jelentése a fiókszolgáltatójának."
- "Írja le a jelentés okát…"
- "Elutasítás és blokkolás"
+ "Írja le az okot…"
+ "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?"
@@ -14,5 +14,5 @@
"Igen, elutasítás és blokkolás"
"Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez a szobához? Ez azt is megakadályozza, hogy %1$s kapcsolatba lépjen Önnel, vagy szobákba hívja."
"Meghívó elutasítása és blokkolás"
- "Elutasítás és blokkolás"
+ "Elutasítás és letiltás"
diff --git a/features/invite/impl/src/main/res/values-nb/translations.xml b/features/invite/impl/src/main/res/values-nb/translations.xml
index 2523466348..31091a1d92 100644
--- a/features/invite/impl/src/main/res/values-nb/translations.xml
+++ b/features/invite/impl/src/main/res/values-nb/translations.xml
@@ -3,7 +3,7 @@
"Du vil ikke se noen meldinger eller rominvitasjoner fra denne brukeren"
"Blokker bruker"
"Rapporter dette rommet til din kontoleverandør."
- "Beskriv årsaken for å rapportere…"
+ "Beskriv årsaken…"
"Avslå og blokker"
"Er du sikker på at du vil takke nei til invitasjonen til å bli med i %1$s?"
"Avvis invitasjon"
diff --git a/features/invite/impl/src/main/res/values-nl/translations.xml b/features/invite/impl/src/main/res/values-nl/translations.xml
index 65051679a2..4abfdda76d 100644
--- a/features/invite/impl/src/main/res/values-nl/translations.xml
+++ b/features/invite/impl/src/main/res/values-nl/translations.xml
@@ -7,4 +7,5 @@
"Chat weigeren"
"Geen uitnodigingen"
"%1$s (%2$s) heeft je uitgenodigd"
+ "Weigeren en blokkeren"
diff --git a/features/invite/impl/src/main/res/values-pl/translations.xml b/features/invite/impl/src/main/res/values-pl/translations.xml
index e8636fc3dc..b20ee321ea 100644
--- a/features/invite/impl/src/main/res/values-pl/translations.xml
+++ b/features/invite/impl/src/main/res/values-pl/translations.xml
@@ -3,7 +3,7 @@
"Nie zobaczysz żadnych wiadomości ani zaproszeń od tego użytkownika"
"Zablokuj użytkownika"
"Zgłoś pokój dostawcy swojego konta."
- "Opisz powód zgłoszenia…"
+ "Opisz powód…"
"Odrzuć i zablokuj"
"Czy na pewno chcesz odrzucić zaproszenie dołączenia do %1$s?"
"Odrzuć zaproszenie"
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 f82f98cb1c..bf0ee63061 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
@@ -3,7 +3,7 @@
"Você não verá nenhuma mensagem ou convite de sala deste usuário"
"Bloquear usuário"
"Denuncie esta sala ao fornecedor da sua conta."
- "Descreva o motivo da denúncia…"
+ "Descreva o motivo para denunciar…"
"Recusar e bloquear"
"Tem certeza de que deseja recusar o convite para entrar em %1$s?"
"Recusar convite"
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 513f14eea6..1bdf28b8f6 100644
--- a/features/invite/impl/src/main/res/values-pt/translations.xml
+++ b/features/invite/impl/src/main/res/values-pt/translations.xml
@@ -3,7 +3,7 @@
"Não vais ver quaisquer mensagens ou convites para sala deste utilizador"
"Bloquear utilizador"
"Denunciar esta sala ao fornecedor da tua conta."
- "Descreve a razão para bloquear…"
+ "Descreve a razão para denunciar…"
"Rejeitar e bloquear"
"Tens a certeza que queres rejeitar o convite para entra em %1$s?"
"Rejeitar convite"
diff --git a/features/invite/impl/src/main/res/values-ru/translations.xml b/features/invite/impl/src/main/res/values-ru/translations.xml
index 90d467e62c..f22b81feb2 100644
--- a/features/invite/impl/src/main/res/values-ru/translations.xml
+++ b/features/invite/impl/src/main/res/values-ru/translations.xml
@@ -3,7 +3,7 @@
"Вы не увидите сообщений или приглашений в комнату от этого пользователя"
"Заблокировать пользователя"
"Сообщите об этой комнате своему поставщику учетной записи."
- "Опишите причину жалобы…"
+ "Опишите причину…"
"Отклонить и заблокировать"
"Вы уверены, что хотите отклонить приглашение в %1$s?"
"Отклонить приглашение"
diff --git a/features/invite/impl/src/main/res/values-sk/translations.xml b/features/invite/impl/src/main/res/values-sk/translations.xml
index a14b5b0dae..826aa023f6 100644
--- a/features/invite/impl/src/main/res/values-sk/translations.xml
+++ b/features/invite/impl/src/main/res/values-sk/translations.xml
@@ -3,7 +3,7 @@
"Od tohto používateľa sa vám nezobrazia žiadne správy ani pozvánky do miestnosti"
"Zablokovať používateľa"
"Nahlásiť túto miestnosť poskytovateľovi účtu."
- "Opíšte dôvod nahlásenia…"
+ "Popíšte dôvod…"
"Odmietnuť a zablokovať"
"Naozaj chcete odmietnuť pozvánku na pripojenie do %1$s?"
"Odmietnuť pozvanie"
diff --git a/features/invite/impl/src/main/res/values-sv/translations.xml b/features/invite/impl/src/main/res/values-sv/translations.xml
index a57336b50e..f44783189a 100644
--- a/features/invite/impl/src/main/res/values-sv/translations.xml
+++ b/features/invite/impl/src/main/res/values-sv/translations.xml
@@ -3,7 +3,7 @@
"Du kommer inte att se några meddelanden eller rumsinbjudningar från den här användaren"
"Blockera användare"
"Rapportera det här rummet till din kontoleverantör."
- "Beskriv skälet för anmälan …"
+ "Beskriv anledningen …"
"Avvisa och blockera"
"Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?"
"Avböj inbjudan"
diff --git a/features/invite/impl/src/main/res/values-tr/translations.xml b/features/invite/impl/src/main/res/values-tr/translations.xml
index 3cfdca3ec6..26ed1cb6d8 100644
--- a/features/invite/impl/src/main/res/values-tr/translations.xml
+++ b/features/invite/impl/src/main/res/values-tr/translations.xml
@@ -10,5 +10,4 @@
"Evet, reddet ve engelle"
"Bu odaya katılma davetini reddetmek istediğinizden emin misiniz? Bu aynı zamanda %1$s sizinle iletişim kurmasını veya sizi odalara davet etmesini de engeller."
"Daveti reddet ve engelle"
- "Reddet ve engelle"
diff --git a/features/invite/impl/src/main/res/values-uk/translations.xml b/features/invite/impl/src/main/res/values-uk/translations.xml
index 9a5aef7298..8752f5707e 100644
--- a/features/invite/impl/src/main/res/values-uk/translations.xml
+++ b/features/invite/impl/src/main/res/values-uk/translations.xml
@@ -3,7 +3,7 @@
"Ви не бачитимете повідомлень або запрошень у кімнату від цього користувача"
"Заблокувати користувача"
"Поскаржитися на цю кімнату постачальнику облікового запису."
- "Опишіть причину скарги…"
+ "Опишіть причину…"
"Відхилити та заблокувати"
"Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?"
"Відхилити запрошення"
diff --git a/features/invite/impl/src/main/res/values-uz/translations.xml b/features/invite/impl/src/main/res/values-uz/translations.xml
index bf1f398712..1db05ad0d9 100644
--- a/features/invite/impl/src/main/res/values-uz/translations.xml
+++ b/features/invite/impl/src/main/res/values-uz/translations.xml
@@ -7,4 +7,5 @@
"Chatni rad etish"
"Takliflar yo\'q"
"%1$s(%2$s ) sizni taklif qildi"
+ "Rad etish va bloklash"
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 2ce5cd929a..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
@@ -18,8 +18,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
-import dev.zacsweers.metro.Inject
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
import io.element.android.features.invitepeople.api.InvitePeopleState
@@ -51,7 +51,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-@Inject
+@AssistedInject
class DefaultInvitePeoplePresenter(
@Assisted private val joinedRoom: JoinedRoom?,
@Assisted private val roomId: RoomId,
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 f827df0e55..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
@@ -17,7 +17,7 @@ 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.Inject
+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
@@ -30,7 +30,7 @@ import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class JoinRoomFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 cb7ad57837..6e123630d6 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
@@ -21,7 +21,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -53,13 +53,13 @@ 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.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
-@Inject
+@AssistedInject
class JoinRoomPresenter(
@Assisted private val roomId: RoomId,
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
@@ -94,9 +94,7 @@ class JoinRoomPresenter(
val roomInfo by remember {
matrixClient.getRoomInfoFlow(roomId)
}.collectAsState(initial = Optional.empty())
- val spaceRoom by remember {
- spaceList.currentSpaceFlow()
- }.collectAsState()
+ 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) }
@@ -279,7 +277,7 @@ private fun RoomPreviewInfo.toContentState(membershipDetails: RoomMembershipDeta
private fun SpaceRoom.toContentState(): ContentState {
return ContentState.Loaded(
roomId = roomId,
- name = name,
+ name = displayName,
topic = topic,
alias = canonicalAlias,
numberOfMembers = numJoinedMembers.toLong(),
@@ -293,7 +291,7 @@ private fun SpaceRoom.toContentState(): ContentState {
joinRule = joinRule,
details = LoadedDetails.Space(
childrenCount = childrenCount,
- heroes = heroes.toPersistentList(),
+ heroes = heroes.toImmutableList(),
)
)
}
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 8eeecab83c..e5f7df67a3 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
@@ -24,7 +24,7 @@ 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
+import kotlinx.collections.immutable.toImmutableList
open class JoinRoomStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -189,7 +189,7 @@ fun aLoadedDetailsSpace(
heroes: List = emptyList(),
) = LoadedDetails.Space(
childrenCount = childrenCount,
- heroes = heroes.toPersistentList()
+ heroes = heroes.toImmutableList()
)
fun aJoinRoomState(
@@ -219,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 6da7aadfe7..38d084c7d3 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
@@ -74,7 +74,7 @@ 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.api.room.join.JoinRule
+import io.element.android.libraries.matrix.api.spaces.SpaceRoomVisibility
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
@@ -567,10 +567,7 @@ private fun DefaultLoadedContent(
subtitle = {
when {
contentState.details is LoadedDetails.Space -> {
- SpaceInfoRow(
- joinRule = contentState.joinRule ?: JoinRule.Public,
- numberOfRooms = contentState.details.childrenCount,
- )
+ SpaceInfoRow(visibility = SpaceRoomVisibility.fromJoinRule(contentState.joinRule))
}
contentState.alias != null -> {
RoomPreviewSubtitleAtom(contentState.alias.value)
diff --git a/features/joinroom/impl/src/main/res/values-be/translations.xml b/features/joinroom/impl/src/main/res/values-be/translations.xml
index 056a4394f7..1986a761b0 100644
--- a/features/joinroom/impl/src/main/res/values-be/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-be/translations.xml
@@ -1,6 +1,6 @@
- "Далучыцца да пакоя"
+ "Далучыцца"
"Націсніце, каб далучыцца"
"%1$s пакуль не падтрымлівае прасторы. Вы можаце атрымаць доступ да прастор праз вэб-старонку."
"Прасторы пакуль не падтрымліваюцца"
diff --git a/features/joinroom/impl/src/main/res/values-bg/translations.xml b/features/joinroom/impl/src/main/res/values-bg/translations.xml
index e9f7c38fcf..b395310bdd 100644
--- a/features/joinroom/impl/src/main/res/values-bg/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-bg/translations.xml
@@ -1,4 +1,5 @@
- "Присъединяване към стаята"
+ "Отхвърляне и блокиране"
+ "Присъединяване"
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..0c00c0c4bd 100644
--- a/features/joinroom/impl/src/main/res/values-cs/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-cs/translations.xml
@@ -15,7 +15,8 @@
"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."
- "Připojit se do místnosti"
+ "Pozván(a)"
+ "Vstoupit"
"Abyste se mohli připojit, musíte být pozváni nebo být členem některého prostoru."
"Zaklepejte a připojte se"
"Povolené znaky %1$d z %2$d"
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 480ccdbb6c..8b5e3c2d6a 100644
--- a/features/joinroom/impl/src/main/res/values-cy/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-cy/translations.xml
@@ -15,7 +15,8 @@
"Mae\'r ystafell hon naill ai drwy wahoddiad yn unig neu efallai y bydd cyfyngiadau ar fynediad ar lefel y gofod."
"Anghofiwch yr ystafell hon"
"Mae angen gwahoddiad arnoch chi er mwyn ymuno â\'r ystafell hon"
- "Ymuno â\'r ystafell"
+ "Gwahoddwyd gan"
+ "Ymuno"
"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"
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..ac1cd26191 100644
--- a/features/joinroom/impl/src/main/res/values-da/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-da/translations.xml
@@ -1,7 +1,7 @@
- "Du blev spærret fra dette rum af %1$s."
- "Du blev spærret fra dette rum"
+ "Du blev spærret af %1$s."
+ "Du blev spærret"
"Årsag: %1$s."
"Annuller anmodning"
"Ja, annullér"
@@ -11,11 +11,12 @@
"Er du sikker på, at du vil afvise invitationen til at deltage i dette rum? Dette forhindrer også %1$s i at kontakte dig eller invitere dig til andre rum."
"Afvis invitation og blokér"
"Afvis og blokér"
- "Deltagelse i rummet fejlede."
- "Dette rum er enten kun for gæster, eller der kan være sat begrænsninger for adgangen på klyngeniveau."
- "Glem dette rum"
- "Du har brug for en invitation for at deltage i dette rum"
- "Deltag i rummet"
+ "Deltagelse fejlede."
+ "Du skal enten inviteres til at deltage, eller der kan være adgangsbegrænsninger."
+ "Glem"
+ "Du har brug for en invitation for at deltage"
+ "Inviteret af"
+ "Deltag"
"Du skal muligvis være inviteret eller være medlem af en klynge for at deltage."
"Send anmodning om at deltage"
"Tilladte tegn %1$d af %2$d"
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 42c2b3fce4..c6d97d6fcb 100644
--- a/features/joinroom/impl/src/main/res/values-de/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-de/translations.xml
@@ -1,7 +1,7 @@
- "Du wurdest von %1$s für diesen Chat gesperrt."
- "Du wurdest für diesen Chat gesperrt"
+ "Du wurdest von %1$s gesperrt."
+ "Du wurdest gesperrt"
"Grund:%1$s."
"Anfrage abbrechen"
"Ja, abbrechen"
@@ -11,11 +11,12 @@
"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 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"
- "Chat beitreten"
+ "Beitritt fehlgeschlagen"
+ "Du musst entweder eingeladen werden, um beizutreten, oder es gibt möglicherweise Zugriffsbeschränkungen."
+ "Vergessen"
+ "Du benötigst eine Einladung, um beizutreten"
+ "Eingeladen von"
+ "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"
diff --git a/features/joinroom/impl/src/main/res/values-el/translations.xml b/features/joinroom/impl/src/main/res/values-el/translations.xml
index 9a7296b93b..b12bbb9374 100644
--- a/features/joinroom/impl/src/main/res/values-el/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-el/translations.xml
@@ -15,7 +15,7 @@
"Αυτή η αίθουσα είναι είτε μόνο για προσκεκλημένους είτε ενδέχεται να υπάρχουν περιορισμοί πρόσβασης σε επίπεδο χώρου."
"Ξεχάστε αυτή την αίθουσα"
"Χρειάζεστε πρόσκληση για να συμμετάσχετε σε αυτή την αίθουσα"
- "Συμμετοχή στην αίθουσα"
+ "Συμμετοχή"
"Ενδέχεται να χρειαστεί να προσκληθείτε ή να είστε μέλος ενός χώρου για να συμμετάσχετε."
"Χτύπα για συμμετοχή"
"Μήνυμα (προαιρετικό)"
diff --git a/features/joinroom/impl/src/main/res/values-es/translations.xml b/features/joinroom/impl/src/main/res/values-es/translations.xml
index b98536b1be..1f905bbbf7 100644
--- a/features/joinroom/impl/src/main/res/values-es/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-es/translations.xml
@@ -15,7 +15,7 @@
"O bien solo se puede acceder a esta sala con invitación, o puede que haya restricciones de acceso a nivel de espacio."
"Olvidar esta sala"
"Necesitas una invitación para unirte a esta sala"
- "Unirse a la sala"
+ "Unirse"
"Es posible que necesites ser invitado o ser miembro de un espacio para poder unirte."
"Enviar solicitud de unión"
"Mensaje (opcional)"
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..b3c51abbf1 100644
--- a/features/joinroom/impl/src/main/res/values-et/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-et/translations.xml
@@ -15,7 +15,8 @@
"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"
- "Liitu jututoaga"
+ "Kutsuja"
+ "Liitu"
"Selle jututoaga liitumiseks sa vajad kutset või pead juba olema kogukonna liige."
"Liitumiseks koputa jututoa uksele"
"Lubatud tähemärke: %1$d / %2$d"
diff --git a/features/joinroom/impl/src/main/res/values-eu/translations.xml b/features/joinroom/impl/src/main/res/values-eu/translations.xml
index e22c65fcb9..2fe6a76104 100644
--- a/features/joinroom/impl/src/main/res/values-eu/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-eu/translations.xml
@@ -4,10 +4,10 @@
"Utzi eskaera bertan behera"
"Bai, utzi bertan behera"
"Eman gonbidapenari ezetza eta blokeatu"
- "Eman ezetza eta blokeatu"
+ "Baztertu eta blokeatu"
"Gelara sartzeak huts egin du."
"Ahaztu gela hau"
- "Sartu gelan"
+ "Elkartu"
"Bidali batzeko eskaera"
"Mezua (aukerakoa)"
"Sartzeko eskaera bidali da"
diff --git a/features/joinroom/impl/src/main/res/values-fa/translations.xml b/features/joinroom/impl/src/main/res/values-fa/translations.xml
index 529177cedb..77468f41ef 100644
--- a/features/joinroom/impl/src/main/res/values-fa/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-fa/translations.xml
@@ -12,7 +12,7 @@
"پیوستن به اتاق شکست خورد."
"فراموشی این اتاق"
"برای پیوستن به این اتاق نیاز به دعوت دارید"
- "پیوستن به اتاق"
+ "پیوستن"
"در زدن برای پیوستن"
"پیام (اختیاری)"
"درخواست پیوستن فرستاده شد"
diff --git a/features/joinroom/impl/src/main/res/values-fi/translations.xml b/features/joinroom/impl/src/main/res/values-fi/translations.xml
index b9d2da01ad..9a8a7e2a16 100644
--- a/features/joinroom/impl/src/main/res/values-fi/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-fi/translations.xml
@@ -1,7 +1,7 @@
- "%1$s antoi sinulle porttikiellon tästä huoneesta."
- "Sinulle on annettu porttikielto tästä huoneesta"
+ "%1$s antoi sinulle porttikiellon."
+ "Sinulle on annettu porttikielto"
"Syy: %1$s."
"Peruuta pyyntö"
"Kyllä, peruuta"
@@ -11,11 +11,11 @@
"Oletko varma, että haluat kieltäytyä kutsusta liittyä tähän huoneeseen? Tämä estää myös käyttäjää %1$s ottamasta sinuun yhteyttä tai kutsumasta sinua huoneisiin."
"Hylkää kutsu ja estä"
"Hylkää ja estä"
- "Huoneeseen liittyminen epäonnistui."
- "Tämä huone on tarkoitettu vain kutsutuille, tai siihen saattaa liittyä rajoituksia tilatasolla."
- "Unohda tämä huone"
- "Tarvitset kutsun liittyäksesi tähän huoneeseen"
- "Liity huoneeseen"
+ "Liittyminen epäonnistui"
+ "Sinun on joko saatava kutsu liittyäksesi tai pääsyyn voi olla rajoituksia."
+ "Unohda"
+ "Tarvitset kutsun liittyäksesi"
+ "Liity"
"Saatat tarvita kutsun tai olla tilan jäsen, jotta voit liittyä."
"Lähetä liittymispyyntö"
"%1$d merkkiä käytetty, %2$d merkkiä sallittu"
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..7ad0317c35 100644
--- a/features/joinroom/impl/src/main/res/values-fr/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-fr/translations.xml
@@ -1,7 +1,7 @@
- "Vous avez été banni de ce salon par %1$s."
- "Vous avez été banni de ce salon"
+ "Vous avez été banni(e) par %1$s."
+ "Vous avez été banni(e)"
"Motif: %1$s."
"Annuler la demande"
"Oui, annuler"
@@ -11,10 +11,11 @@
"Êtes-vous sûr de vouloir refuser l’invitation à rejoindre ce salon ? Cela empêchera également %1$s de vous contacter ou de vous inviter dans les salons."
"Refuser l’invitation et bloquer"
"Refuser et bloquer"
- "Rejoindre le salon a échoué."
- "Ce salon est accessible uniquement sur invitation ou il peut y avoir des restrictions d’accès au niveau de l’espace."
- "Oublier ce salon"
- "Vous avez besoin d’une invitation pour rejoindre ce salon"
+ "L’opération a échoué."
+ "Soit vous devez être invité(e) pour rejoindre, soit il peut y avoir des restrictions d’accès."
+ "Oublier"
+ "Vous avez besoin d’une invitation pour pouvoir rejoindre"
+ "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..8fafb9aa7a 100644
--- a/features/joinroom/impl/src/main/res/values-hu/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-hu/translations.xml
@@ -10,12 +10,13 @@
"Igen, elutasítás és blokkolás"
"Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez a szobához? Ez azt is megakadályozza, hogy %1$s kapcsolatba lépjen Önnel, vagy szobákba hívja."
"Meghívó elutasítása és blokkolás"
- "Elutasítás és blokkolás"
+ "Elutasítás és letiltás"
"A szobához való csatlakozás sikertelen."
"Ebbe a szobába csak meghívóval vagy tértagsággal lehet belépni."
"Szoba elfelejtése"
"Meghívóra van szüksége ahhoz, hogy csatlakozzon ehhez a szobához"
- "Csatlakozás a szobához"
+ "Meghívta:"
+ "Csatlakozás"
"A csatlakozáshoz meghívásra vagy tértagságra lehet szüksége."
"Kopogtasson a csatlakozáshoz"
"Engedélyezett karakterek: %1$d / %2$d"
diff --git a/features/joinroom/impl/src/main/res/values-in/translations.xml b/features/joinroom/impl/src/main/res/values-in/translations.xml
index 16c078c6ff..59e3aba259 100644
--- a/features/joinroom/impl/src/main/res/values-in/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-in/translations.xml
@@ -15,7 +15,7 @@
"Ruangan ini hanya untuk undangan atau mungkin ada pembatasan akses pada tingkat space."
"Lupakan ruangan ini"
"Anda memerlukan undangan untuk bergabung dalam ruangan ini"
- "Gabung dengan ruangan"
+ "Gabung"
"Anda mungkin perlu diundang atau menjadi anggota space untuk bergabung."
"Ketuk untuk bergabung"
"Pesan (opsional)"
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 1ea811d8dc..deb025db27 100644
--- a/features/joinroom/impl/src/main/res/values-it/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-it/translations.xml
@@ -15,7 +15,7 @@
"Questa stanza è solo su invito o potrebbero esserci delle restrizioni all\'accesso al livello dello spazio."
"Dimentica questa stanza"
"Hai bisogno di un invito per entrare in questa stanza"
- "Entra nella stanza"
+ "Entra"
"Potrebbe essere necessario essere invitati o essere membro di uno spazio per partecipare."
"Bussa per partecipare"
"Caratteri consentiti: %1$d di %2$d"
diff --git a/features/joinroom/impl/src/main/res/values-ka/translations.xml b/features/joinroom/impl/src/main/res/values-ka/translations.xml
new file mode 100644
index 0000000000..038b80722b
--- /dev/null
+++ b/features/joinroom/impl/src/main/res/values-ka/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "გაწევრიანება"
+
diff --git a/features/joinroom/impl/src/main/res/values-ko/translations.xml b/features/joinroom/impl/src/main/res/values-ko/translations.xml
index b6edc8447e..80225ac402 100644
--- a/features/joinroom/impl/src/main/res/values-ko/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-ko/translations.xml
@@ -15,7 +15,7 @@
"이 방은 초대 전용이거나 스페이스 수준에서 액세스 제한이 있을 수 있습니다."
"이 방 지우기"
"이 방에 참여하려면 초대장이 필요합니다."
- "방에 참여하기"
+ "참가하기"
"참여하려면 초대 또는 스페이스의 회원이어야 할 수 있습니다."
"가입 요청 보내기"
"%2$d의 %1$d 문자가 허용됨"
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 a349def982..5c4873bc57 100644
--- a/features/joinroom/impl/src/main/res/values-nb/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-nb/translations.xml
@@ -1,7 +1,7 @@
- "Du ble utestengt fra dette rommet av %1$s."
- "Du ble utestengt fra dette rommet"
+ "Du ble utestengt av %1$s."
+ "Du ble utestengt"
"Årsak: %1$s."
"Avbryt forespørsel"
"Ja, avbryt"
@@ -11,11 +11,12 @@
"Er du sikker på at du vil avslå invitasjonen til å bli med i dette rommet? Dette vil også forhindre %1$s fra å kontakte deg eller invitere deg til rom."
"Avslå invitasjon og blokker"
"Avslå og blokker"
- "Å bli med i rommet mislyktes."
- "Dette rommet er enten kun for inviterte, eller det kan være begrensninger for tilgang på områdenivå."
- "Glem dette rommet"
- "Du trenger en invitasjon for å bli med i dette rommet"
- "Bli med i rommet"
+ "Kunne ikke bli med"
+ "Du må enten bli invitert til å bli med, eller så kan det være begrensninger på tilgangen."
+ "Glem"
+ "Du trenger en invitasjon for å bli med"
+ "Invitert av"
+ "Bli med"
"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"
diff --git a/features/joinroom/impl/src/main/res/values-nl/translations.xml b/features/joinroom/impl/src/main/res/values-nl/translations.xml
index 3132629fc0..36d480b260 100644
--- a/features/joinroom/impl/src/main/res/values-nl/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-nl/translations.xml
@@ -2,8 +2,9 @@
"Reden: %1$s."
"Verzoek annuleren"
+ "Weigeren en blokkeren"
"Deze kamer vergeten"
- "Toetreden tot de kamer"
+ "Deelnemen"
"Klop om deel te nemen"
"Bericht (optioneel)"
"Je ontvangt een uitnodiging om deel te nemen aan de kamer als je aanvraag wordt geaccepteerd."
diff --git a/features/joinroom/impl/src/main/res/values-pl/translations.xml b/features/joinroom/impl/src/main/res/values-pl/translations.xml
index 21042e2903..dea02ea7cd 100644
--- a/features/joinroom/impl/src/main/res/values-pl/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-pl/translations.xml
@@ -15,7 +15,7 @@
"Ten pokój wymaga zaproszenia lub jest ograniczony z poziomu przestrzeni."
"Zapomnij o tym pokoju"
"Potrzebujesz zaproszenia, aby dołączyć do tego pokoju"
- "Dołącz do pokoju"
+ "Dołącz"
"Aby dołączyć, musisz uzyskać zaproszenie lub być członkiem danej przestrzeni."
"Wyślij prośbę o dołączenie"
"Dozwolone znaki %1$d z %2$d"
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 38f91512d7..ed553bd93a 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
@@ -15,7 +15,7 @@
"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"
+ "Entrar"
"Talvez você precise ser convidado ou ser membro de um espaço para participar."
"Enviar solicitação para entrar"
"%1$d de %2$d caráteres permitidos"
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 5bac83b9d2..4ed5e4219f 100644
--- a/features/joinroom/impl/src/main/res/values-pt/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-pt/translations.xml
@@ -15,7 +15,8 @@
"A entrada nesta sala ou está limitada a convites ou a alguma configuração de espaço."
"Esquecer esta sala"
"Precisas de um convite para entrares nesta sala"
- "Entrar na sala"
+ "Convidado por"
+ "Entrar"
"Podes ter que ser convidado ou pertenceres a um espaço para poderes entrar."
"Bater à porta"
"%1$d de %2$d caracteres permitidos"
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 ca7764aa62..8326ce06bf 100644
--- a/features/joinroom/impl/src/main/res/values-ro/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-ro/translations.xml
@@ -1,6 +1,6 @@
- "Ai fost exclus din această cameră de către %1$s."
+ "Ați fost exclus din această cameră de către %1$s."
"Ați fost exclus din această cameră."
"Motiv: %1$s."
"Anulați cererea"
@@ -15,7 +15,8 @@
"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"
+ "Invitat de"
+ "Alăturați-vă"
"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"
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 4c87355000..9b50f35da4 100644
--- a/features/joinroom/impl/src/main/res/values-ru/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-ru/translations.xml
@@ -15,9 +15,11 @@
"Доступ в эту комнату возможен только по приглашениям или может быть ограничен на уровне пространства."
"Забыть эту комнату"
"Вам необходимо приглашение для того, чтобы присоединиться к этой комнате"
- "Присоединиться к комнате"
+ "Приглашен"
+ "Присоединиться"
"Чтобы присоединиться, вам необходимо приглашение или быть участником сообщества."
"Отправить запрос на присоединение"
+ "Разрешенные символы %1$d %2$d"
"Сообщение (опционально)"
"Вы получите приглашение присоединиться к комнате, как только ваш запрос будет принят."
"Запрос на присоединение отправлен"
diff --git a/features/joinroom/impl/src/main/res/values-sk/translations.xml b/features/joinroom/impl/src/main/res/values-sk/translations.xml
index 5ee2899674..35cc31af32 100644
--- a/features/joinroom/impl/src/main/res/values-sk/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-sk/translations.xml
@@ -15,7 +15,7 @@
"Táto miestnosť je buď len pre pozvaných, alebo môžu existovať obmedzenia na prístup na úrovni priestoru."
"Zabudnúť túto miestnosť"
"Potrebujete pozvanie, aby ste sa mohli pripojiť k tejto miestnosti"
- "Pripojiť sa do miestnosti"
+ "Pripojiť sa"
"Možno budete musieť byť pozvaní alebo byť členom priestoru, aby ste sa mohli pripojiť."
"Zaklopaním sa pripojíte"
"Povolené znaky %1$d z %2$d"
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 78924f1cd6..11af0da734 100644
--- a/features/joinroom/impl/src/main/res/values-sv/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-sv/translations.xml
@@ -15,7 +15,7 @@
"Detta rum är antingen endast för inbjudna eller så kan det finnas begränsningar för åtkomst på utrymmesnivå."
"Glöm det här rummet"
"Du behöver en inbjudan för att gå med i detta rum"
- "Gå med i rummet"
+ "Gå med"
"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"
diff --git a/features/joinroom/impl/src/main/res/values-tr/translations.xml b/features/joinroom/impl/src/main/res/values-tr/translations.xml
index fb3edc84b2..abd08d3c86 100644
--- a/features/joinroom/impl/src/main/res/values-tr/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-tr/translations.xml
@@ -10,12 +10,11 @@
"Evet, reddet ve engelle"
"Bu odaya katılma davetini reddetmek istediğinizden emin misiniz? Bu aynı zamanda %1$s sizinle iletişim kurmasını veya sizi odalara davet etmesini de engeller."
"Daveti reddet ve engelle"
- "Reddet ve engelle"
"Odaya katılım başarısız oldu."
"Bu odaya yalnızca davetle girilebilir veya alan düzeyinde erişim kısıtlamaları olabilir."
"Bu odayı unut"
"Bu odaya katılmak için bir davete ihtiyacınız var"
- "Odaya katıl"
+ "Katıl"
"Katılmak için davet edilmeniz veya bir alana üye olmanız gerekebilir."
"Katılma isteği gönder"
"Mesaj (isteğe bağlı)"
diff --git a/features/joinroom/impl/src/main/res/values-uk/translations.xml b/features/joinroom/impl/src/main/res/values-uk/translations.xml
index 6bdd39807b..90d5b01441 100644
--- a/features/joinroom/impl/src/main/res/values-uk/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-uk/translations.xml
@@ -15,7 +15,7 @@
"Ця кімната доступна лише за запрошенням або на рівні простору можуть бути обмеження доступу."
"Забути цю кімнату"
"Вам потрібне запрошення, щоб приєднатися до цієї кімнати"
- "Приєднатися до кімнати"
+ "Доєднатися"
"Можливо, вам знадобиться отримати запрошення або стати учасником простору, щоб приєднатися."
"Постукати, щоб приєднатися"
"Дозволені символи %1$d з %2$d"
diff --git a/features/joinroom/impl/src/main/res/values-ur/translations.xml b/features/joinroom/impl/src/main/res/values-ur/translations.xml
index af34c73710..4bb703073c 100644
--- a/features/joinroom/impl/src/main/res/values-ur/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-ur/translations.xml
@@ -1,6 +1,6 @@
- "کمرے میں شامل ہوں"
+ "شامل ہوں"
"شامل ہونے کی درخواست بھیجیں"
"%1$s ابھی تک خالی جگہوں کی حمایت نہیں کرتا۔ آپ جال پر خالی جگہوں تک رسائی حاصل کرسکتے ہیں۔"
"ابھی تک جگہیں تعاون یافتہ نہیں"
diff --git a/features/joinroom/impl/src/main/res/values-uz/translations.xml b/features/joinroom/impl/src/main/res/values-uz/translations.xml
index 2fbb5e38d7..e8c50922c4 100644
--- a/features/joinroom/impl/src/main/res/values-uz/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-uz/translations.xml
@@ -4,7 +4,8 @@
"Ha, bekor qiling"
"Bu xonaga qo‘shilish so‘rovingizni bekor qilishni xohlayotganingizga ishonchingiz komilmi?"
"Qo‘shilish so‘rovini bekor qilish"
- "Xonaga qoʻshilish"
+ "Rad etish va bloklash"
+ "Qo\'shilish"
"Qoʻshilish soʻrovini yuborish"
"Xabar (ixtiyoriy)"
"Agar so‘rovingiz qabul qilinsa, xonaga qo‘shilish taklifini olasiz."
diff --git a/features/joinroom/impl/src/main/res/values-zh-rTW/translations.xml b/features/joinroom/impl/src/main/res/values-zh-rTW/translations.xml
index a66510111c..aa7bbc261d 100644
--- a/features/joinroom/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-zh-rTW/translations.xml
@@ -15,7 +15,7 @@
"此聊天室僅有受邀者才能進入,或是在空間層級有存取限制。"
"忘記此聊天室"
"您需要獲得邀請才能加入此聊天室"
- "加入聊天室"
+ "加入"
"您可能需要被邀請成為空間的成員才能加入。"
"傳送加入請求"
"允許的字元 %1$d 中的 %2$d"
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 05c52c9293..52361e01aa 100644
--- a/features/joinroom/impl/src/main/res/values-zh/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-zh/translations.xml
@@ -15,7 +15,8 @@
"要么此房间仅限受邀者,要么可能在空间层级有加入限制。"
"忘记这个房间"
"你需要邀请才能加入这个房间"
- "加入聊天室"
+ "受邀于"
+ "加入"
"您可能需要受到邀请或成为某个空间的成员才能加入。"
"加入聊天室"
"允许的字符数量 %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 3d6a319aa6..0e1787fc0b 100644
--- a/features/joinroom/impl/src/main/res/values/localazy.xml
+++ b/features/joinroom/impl/src/main/res/values/localazy.xml
@@ -1,7 +1,7 @@
- "You were banned from this room by %1$s."
- "You were banned from this room"
+ "You were banned by %1$s."
+ "You were banned"
"Reason: %1$s."
"Cancel request"
"Yes, cancel"
@@ -11,12 +11,12 @@
"Are you sure you want to decline the invite to join this room? This will also prevent %1$s from contacting you or inviting you to rooms."
"Decline invite & block"
"Decline and block"
- "Joining the room failed."
- "This room is either invite-only or there might be restrictions to access at space level."
- "Forget this room"
- "You need an invite in order to join this room"
+ "Joining failed"
+ "You either need to be invited to join or there might be restrictions to access."
+ "Forget"
+ "You need an invite in order to join"
"Invited by"
- "Join room"
+ "Join"
"You may need to be invited or be a member of a space in order to join."
"Send request to join"
"Allowed characters %1$d of %2$d"
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 574c310d1f..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
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 2fa3542174..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
@@ -13,12 +13,12 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.RoomScope
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class KnockRequestsListNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
diff --git a/features/knockrequests/impl/src/main/res/values-da/translations.xml b/features/knockrequests/impl/src/main/res/values-da/translations.xml
index 30ac6a8d30..795cda658c 100644
--- a/features/knockrequests/impl/src/main/res/values-da/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-da/translations.xml
@@ -32,5 +32,5 @@
"Se alle"
"Accepter"
"%1$s ønsker at deltage i dette rum"
- "Se"
+ "Vis"
diff --git a/features/knockrequests/impl/src/main/res/values-el/translations.xml b/features/knockrequests/impl/src/main/res/values-el/translations.xml
index 2066b2e293..5c8bec16f8 100644
--- a/features/knockrequests/impl/src/main/res/values-el/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-el/translations.xml
@@ -32,5 +32,4 @@
"Προβολή όλων"
"Αποδοχή"
"%1$s θέλει να συμμετάσχει σε αυτή την αίθουσα"
- "Προβολή"
diff --git a/features/knockrequests/impl/src/main/res/values-es/translations.xml b/features/knockrequests/impl/src/main/res/values-es/translations.xml
index ea9f74544d..8f9ab72b70 100644
--- a/features/knockrequests/impl/src/main/res/values-es/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-es/translations.xml
@@ -32,5 +32,4 @@
"Ver todo"
"Aceptar"
"%1$s quiere unirse a esta sala"
- "Ver"
diff --git a/features/knockrequests/impl/src/main/res/values-nl/translations.xml b/features/knockrequests/impl/src/main/res/values-nl/translations.xml
index 46787d05e0..a78d6222ca 100644
--- a/features/knockrequests/impl/src/main/res/values-nl/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-nl/translations.xml
@@ -1,4 +1,5 @@
"Accepteren"
+ "Bekijken"
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 4ccedac193..d6fcebb5ab 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
@@ -32,5 +32,5 @@
"Ver tudo"
"Aceitar"
"%1$s quer entrar nesta sala"
- "Ver"
+ "Visualizar"
diff --git a/features/knockrequests/impl/src/main/res/values-tr/translations.xml b/features/knockrequests/impl/src/main/res/values-tr/translations.xml
index b6a070ee08..445d2aa2dd 100644
--- a/features/knockrequests/impl/src/main/res/values-tr/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-tr/translations.xml
@@ -32,5 +32,4 @@
"Tümünü görüntüle"
"Kabul et"
"%1$s bu odaya katılmak istiyor"
- "Görüntüle"
diff --git a/features/knockrequests/impl/src/main/res/values-uk/translations.xml b/features/knockrequests/impl/src/main/res/values-uk/translations.xml
index 55c973fbf4..11a9b7fad4 100644
--- a/features/knockrequests/impl/src/main/res/values-uk/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-uk/translations.xml
@@ -33,5 +33,5 @@
"Переглянути все"
"Прийняти"
"%1$s хоче приєднатися до цієї кімнати"
- "Перегляд"
+ "Переглянути"
diff --git a/features/knockrequests/impl/src/main/res/values-uz/translations.xml b/features/knockrequests/impl/src/main/res/values-uz/translations.xml
index d7e6705e67..8cad1166df 100644
--- a/features/knockrequests/impl/src/main/res/values-uz/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-uz/translations.xml
@@ -1,4 +1,5 @@
"Qabul qiling"
+ "Ko\'rish"
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/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 06305411ec..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
@@ -17,7 +17,7 @@ import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -28,7 +28,7 @@ import io.element.android.libraries.architecture.createNode
import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class DependenciesFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 3fca4b13dd..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
@@ -14,14 +14,14 @@ 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.Inject
+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
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class DependenciesDetailsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 ca7caf7014..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
@@ -15,12 +15,12 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class DependencyLicensesListNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 7c36a4f1ef..0af5d1cc47 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
@@ -20,7 +20,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.extensions.runCatchingExceptions
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
@Inject
class DependencyLicensesListPresenter(
@@ -37,7 +37,7 @@ class DependencyLicensesListPresenter(
var filter by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
runCatchingExceptions {
- licenses = AsyncData.Success(licensesProvider.provides().toPersistentList())
+ licenses = AsyncData.Success(licensesProvider.provides().toImmutableList())
}.onFailure {
licenses = AsyncData.Failure(it)
}
@@ -50,7 +50,7 @@ class DependencyLicensesListPresenter(
it.safeName.contains(safeFilter, ignoreCase = true) ||
it.groupId.contains(safeFilter, ignoreCase = true) ||
it.artifactId.contains(safeFilter, ignoreCase = true)
- }.toPersistentList())
+ }.toImmutableList())
} else {
filteredLicenses = licenses
}
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 06d7885be1..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
@@ -14,11 +14,11 @@ import com.google.accompanist.permissions.rememberMultiplePermissionsState
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 dev.zacsweers.metro.Inject
@Suppress("unused")
-@Inject
+@AssistedInject
class DefaultPermissionsPresenter(
@Assisted private val permissions: List
) : PermissionsPresenter {
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 a0fc18cee8..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
@@ -14,7 +14,7 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
@@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class SendLocationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 f8d78b96e2..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
@@ -17,7 +17,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.launch
-@Inject
+@AssistedInject
class SendLocationPresenter(
permissionsPresenterFactory: PermissionsPresenter.Factory,
private val room: JoinedRoom,
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 c0a6c5b6ad..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
@@ -14,7 +14,7 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.features.location.api.ShowLocationEntryPoint
@@ -23,7 +23,7 @@ import io.element.android.libraries.di.RoomScope
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class ShowLocationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 b304505367..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
@@ -16,7 +16,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 +26,7 @@ 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
-@Inject
+@AssistedInject
class ShowLocationPresenter(
@Assisted private val location: Location,
@Assisted private val description: String?,
diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts
index 011d4919e7..998b763804 100644
--- a/features/lockscreen/impl/build.gradle.kts
+++ b/features/lockscreen/impl/build.gradle.kts
@@ -39,6 +39,7 @@ 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)
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 f572ef5cfb..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
@@ -16,7 +16,7 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -29,7 +29,7 @@ import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class LockScreenFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt
index 78356fdddf..e9ee9a9903 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt
@@ -8,7 +8,7 @@
package io.element.android.features.lockscreen.impl.pin.model
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
data class PinEntry(
val digits: ImmutableList,
@@ -17,7 +17,7 @@ data class PinEntry(
fun createEmpty(size: Int): PinEntry {
val digits = List(size) { PinDigit.Empty }
return PinEntry(
- digits = digits.toPersistentList()
+ digits = digits.toImmutableList()
)
}
}
@@ -37,7 +37,7 @@ data class PinEntry(
newDigits[index] = PinDigit.Filled(char)
}
}
- return copy(digits = newDigits.toPersistentList())
+ return copy(digits = newDigits.toImmutableList())
}
fun deleteLast(): PinEntry {
@@ -46,7 +46,7 @@ data class PinEntry(
newDigits.indexOfLast { it is PinDigit.Filled }.also { lastFilled ->
newDigits[lastFilled] = PinDigit.Empty
}
- return copy(digits = newDigits.toPersistentList())
+ return copy(digits = newDigits.toImmutableList())
}
fun addDigit(digit: Char): PinEntry {
@@ -55,7 +55,7 @@ data class PinEntry(
newDigits.indexOfFirst { it is PinDigit.Empty }.also { firstEmpty ->
newDigits[firstEmpty] = PinDigit.Filled(digit)
}
- return copy(digits = newDigits.toPersistentList())
+ return copy(digits = newDigits.toImmutableList())
}
fun clear(): PinEntry {
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 b32ca14c02..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,13 +14,12 @@ 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 dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -30,19 +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)
-@Inject
+@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,
@@ -50,7 +50,7 @@ class LockScreenSettingsFlowNode(
) {
sealed interface NavTarget : Parcelable {
@Parcelize
- data object Unknown : NavTarget
+ data object Loading : NavTarget
@Parcelize
data object Unlock : NavTarget
@@ -94,6 +94,9 @@ class LockScreenSettingsFlowNode(
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() {
@@ -113,7 +116,6 @@ class LockScreenSettingsFlowNode(
}
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 e821520509..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
@@ -14,12 +14,12 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class LockScreenSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 e3b1704538..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
@@ -18,7 +18,7 @@ import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -32,7 +32,7 @@ import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class LockScreenSetupFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 deebd9d10a..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
@@ -15,12 +15,12 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class SetupBiometricNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 94cad2cf87..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
@@ -13,12 +13,12 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class SetupPinNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 e5483f12a3..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
@@ -15,12 +15,12 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class PinUnlockNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts
index 314907c04a..690fe13578 100644
--- a/features/login/impl/build.gradle.kts
+++ b/features/login/impl/build.gradle.kts
@@ -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)
@@ -56,5 +57,6 @@ dependencies {
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)
}
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 a5522db870..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
@@ -24,7 +24,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.singleTop
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -51,7 +51,7 @@ import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class LoginFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
@@ -87,7 +87,7 @@ class LoginFlowNode(
// 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/login/LoginHelper.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt
index 70a0d97781..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
@@ -94,9 +94,14 @@ class LoginHelper(
}
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
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/QrCodeLoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt
index 2575d521ab..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
@@ -23,7 +23,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.replace
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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.QrCodeLoginGraph
@@ -49,7 +49,7 @@ import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class QrCodeLoginFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 1efb7a6afc..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
@@ -16,12 +16,12 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class ChangeAccountProviderNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 e555a7d075..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
@@ -16,13 +16,13 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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.matrix.api.auth.OidcDetails
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class ChooseAccountProviderNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 2cab4bdbe9..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
@@ -16,7 +16,7 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.matrix.api.auth.OidcDetails
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class ConfirmAccountProviderNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 fb15191abe..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
@@ -13,12 +13,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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
-@Inject
+@AssistedInject
class ConfirmAccountProviderPresenter(
@Assisted private val params: Params,
private val accountProviderDataSource: AccountProviderDataSource,
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 94b249bc8e..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
@@ -16,7 +16,7 @@ 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.Inject
+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
@@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class CreateAccountNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 b713020acf..a3aec0eb81 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
@@ -15,7 +15,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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
@@ -31,7 +31,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import kotlin.time.Duration.Companion.seconds
-@Inject
+@AssistedInject
class CreateAccountPresenter(
@Assisted private val url: String,
private val authenticationService: MatrixAuthenticationService,
@@ -82,7 +82,7 @@ class CreateAccountPresenter(
tryOrNull {
// Wait until the session is verified
val client = clientProvider.getOrRestore(sessionId).getOrThrow()
- val sessionVerificationService = client.sessionVerificationService()
+ val sessionVerificationService = client.sessionVerificationService
withTimeout(10.seconds) { sessionVerificationService.sessionVerifiedStatus.first { it.isVerified() } }
}
loggedInState.value = AsyncAction.Success(sessionId)
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 e4a5992585..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
@@ -14,11 +14,11 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class LoginPasswordNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 29fed1adbd..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
@@ -16,7 +16,7 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.matrix.api.auth.OidcDetails
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class OnBoardingNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
@@ -97,6 +97,7 @@ class OnBoardingNode(
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 cec124d587..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
@@ -18,7 +18,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,9 +27,10 @@ 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
-@Inject
+@AssistedInject
class OnBoardingPresenter(
@Assisted private val params: OnBoardingNode.Params,
private val buildMeta: BuildMeta,
@@ -38,6 +39,7 @@ class OnBoardingPresenter(
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val loginHelper: LoginHelper,
private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider,
+ private val sessionStore: SessionStore,
) : Presenter {
@AssistedFactory
interface Factory {
@@ -86,6 +88,10 @@ class OnBoardingPresenter(
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()
@@ -109,6 +115,7 @@ class OnBoardingPresenter(
}
return OnBoardingState(
+ isAddingAccount = isAddingAccount,
productionApplicationName = buildMeta.productionApplicationName,
defaultAccountProvider = defaultAccountProvider,
mustChooseAccountProvider = mustChooseAccountProvider,
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 c2896d4ea7..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
@@ -12,6 +12,7 @@ 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,
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 cc41e64480..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
@@ -23,10 +23,16 @@ open class OnBoardingStateProvider : PreviewParameterProvider {
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,
@@ -39,6 +45,7 @@ fun anOnBoardingState(
loginMode: AsyncData = AsyncData.Uninitialized,
eventSink: (OnBoardingEvents) -> Unit = {},
) = OnBoardingState(
+ isAddingAccount = isAddingAccount,
productionApplicationName = productionApplicationName,
defaultAccountProvider = defaultAccountProvider,
mustChooseAccountProvider = mustChooseAccountProvider,
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 4c44ee132a..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
@@ -38,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
@@ -58,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,
@@ -67,6 +70,52 @@ 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,
@@ -79,29 +128,31 @@ fun OnBoardingView(
} else {
OnBoardingContent(state = state)
}
- LoginModeView(
- loginMode = state.loginMode,
- onClearError = {
- state.eventSink(OnBoardingEvents.ClearError)
- },
- onLearnMoreClick = onLearnMoreClick,
- onOidcDetails = onOidcDetails,
- onNeedLoginPassword = onNeedLoginPassword,
- onCreateAccountContinue = onCreateAccountContinue,
- )
+ 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(
@@ -226,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,
+ )
+ }
}
}
}
@@ -258,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 d7d9e7c826..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
@@ -14,13 +14,13 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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)
-@Inject
+@AssistedInject
class QrCodeConfirmationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 f7ebce1106..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
@@ -14,7 +14,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -22,7 +22,7 @@ import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.core.meta.BuildMeta
@ContributesNode(QrCodeLoginScope::class)
-@Inject
+@AssistedInject
class QrCodeErrorNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 3e3a906a04..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
@@ -14,12 +14,12 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.di.QrCodeLoginScope
@ContributesNode(QrCodeLoginScope::class)
-@Inject
+@AssistedInject
class QrCodeIntroNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 0fd50bfcc3..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
@@ -14,13 +14,13 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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)
-@Inject
+@AssistedInject
class QrCodeScanNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 5011201b8b..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
@@ -16,12 +16,12 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class SearchAccountProviderNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
diff --git a/features/login/impl/src/main/res/values-be/translations.xml b/features/login/impl/src/main/res/values-be/translations.xml
index 6fde5f6622..307ccf7263 100644
--- a/features/login/impl/src/main/res/values-be/translations.xml
+++ b/features/login/impl/src/main/res/values-be/translations.xml
@@ -30,6 +30,7 @@
"Сардэчна запрашаем!"
"Увайсці ў %1$s"
"Увайсці ўручную"
+ "Увайсці ў %1$s"
"Увайсці з QR-кодам"
"Стварыць уліковы запіс"
"Сардэчна запрашаем у самы хуткі %1$s. Перавага ў хуткасці і прастаце."
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..6ccc7c9129 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,18 +9,25 @@
"На път сте да влезете в %s"
"Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли."
"На път сте да създадете акаунт в %s"
+ "Matrix.org е голям, безплатен сървър в публичната мрежа на Matrix за сигурна, децентрализирана комуникация, управляван от фондация Matrix.org."
"Друг"
"Използвайте друг доставчик на акаунт, като например собствен частен сървър или работен акаунт."
"Промяна на доставчика на акаунт"
+ "Не можахме да достигнем този сървър. Моля, проверете дали сте въвели правилно URL адреса на сървъра. Ако URL адресът е правилен, свържете се с администратора на вашия сървър за допълнителна помощ."
+ "URL адрес на сървъра"
"Какъв е адресът на вашия сървър?"
+ "Изберете своя сървър"
"Създаване на акаунт"
"Този акаунт бе деактивиран."
"Неправилно потребителско име и/или парола"
+ "Това не е валиден потребителски идентификатор. Очакван формат: ‘@user:homeserver.org’"
+ "Избраният сървър не поддържа влизане с парола или OIDC. Моля, свържете се с вашия администратор или изберете друг сървър."
"Въведете своите данни"
"Matrix е отворена мрежа за сигурна, децентрализирана комуникация."
"Добре дошли отново!"
"Влизане в %1$s"
"Влизане ръчно"
+ "Влизане в %1$s"
"Влизане с QR код"
"Създаване на акаунт"
"Добре дошли в най-бързия %1$s досега. Супер зареден за скорост и простота."
@@ -28,6 +36,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 e5d277c6eb..6ffd9a84d4 100644
--- a/features/login/impl/src/main/res/values-cs/translations.xml
+++ b/features/login/impl/src/main/res/values-cs/translations.xml
@@ -39,7 +39,7 @@
"Přihlaste se k %1$s"
"Verze %1$s"
"Ruční přihlášení"
- "Přihlásit se do %1$s"
+ "Přihlaste se k %1$s"
"Přihlásit se pomocí QR kódu"
"Vytvořit účet"
"Vítejte v dosud nejrychlejším %1$su. Vylepšený pro rychlost a jednoduchost."
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 da0da5257a..cb3459f62b 100644
--- a/features/login/impl/src/main/res/values-de/translations.xml
+++ b/features/login/impl/src/main/res/values-de/translations.xml
@@ -39,7 +39,7 @@
"Anmelden bei %1$s"
"Version %1$s"
"Manuell anmelden"
- "Bei %1$s anmelden"
+ "Anmelden bei %1$s"
"Mit QR-Code anmelden"
"Konto erstellen"
"Willkommen beim schnellsten %1$s aller Zeiten. Optimiert für Geschwindigkeit und Einfachheit."
diff --git a/features/login/impl/src/main/res/values-eo/translations.xml b/features/login/impl/src/main/res/values-eo/translations.xml
index 47e9b3830a..d8ecc9f5d6 100644
--- a/features/login/impl/src/main/res/values-eo/translations.xml
+++ b/features/login/impl/src/main/res/values-eo/translations.xml
@@ -1,4 +1,4 @@
- "A secure connection could not be made to the new device. Your existing connected devices are still safe and you don\'t need to worry about them."
+ "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-et/translations.xml b/features/login/impl/src/main/res/values-et/translations.xml
index 83df34e10a..3e3e6add37 100644
--- a/features/login/impl/src/main/res/values-et/translations.xml
+++ b/features/login/impl/src/main/res/values-et/translations.xml
@@ -39,7 +39,7 @@
"Logi sisse serverisse %1$s"
"Versioon %1$s"
"Logi sisse käsitsi"
- "Logi sisse teenusesse %1$s"
+ "Logi sisse serverisse %1$s"
"Logi sisse QR-koodi alusel"
"Loo kasutajakonto"
"Läbi aegade kiireim ja mugavaim %1$s."
diff --git a/features/login/impl/src/main/res/values-eu/translations.xml b/features/login/impl/src/main/res/values-eu/translations.xml
index ae0079ad39..355e63546c 100644
--- a/features/login/impl/src/main/res/values-eu/translations.xml
+++ b/features/login/impl/src/main/res/values-eu/translations.xml
@@ -28,6 +28,7 @@
"Hasi saioa %1$s(e)n"
"%1$s bertsioa"
"Hasi saioa eskuz"
+ "Hasi saioa %1$s(e)n"
"Hasi saioa QR kodearekin"
"Sortu kontua"
"Ongi etorri inoizko %1$s azkarrenera. Abiaduraz eta sinpletasunaz gainkargatua."
diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml
index 26936afbd7..946fec802e 100644
--- a/features/login/impl/src/main/res/values-fr/translations.xml
+++ b/features/login/impl/src/main/res/values-fr/translations.xml
@@ -39,7 +39,7 @@
"Connectez-vous à %1$s"
"Version %1$s"
"Se connecter manuellement"
- "Se connecter à %1$s"
+ "Connectez-vous à %1$s"
"Se connecter avec un QR code"
"Créer un compte"
"Bienvenue dans l’application %1$s la plus rapide de tous les temps. Boosté pour plus de rapidité et de simplicité."
diff --git a/features/login/impl/src/main/res/values-ka/translations.xml b/features/login/impl/src/main/res/values-ka/translations.xml
index f3e46faf19..54e277ec12 100644
--- a/features/login/impl/src/main/res/values-ka/translations.xml
+++ b/features/login/impl/src/main/res/values-ka/translations.xml
@@ -29,6 +29,7 @@
"კეთილი იყოს თქვენი მობრძანება!"
"შესვლა %1$s-ში"
"ხელით შესვლა"
+ "შესვლა %1$s-ში"
"შესვლა QR კოდით"
"ანგარიშის შექმნა"
"კეთილი იყოს თქვენი მობრძანება უსწრაფეს %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
index 2c8e688994..7c099f1759 100644
--- a/features/login/impl/src/main/res/values-ko/translations.xml
+++ b/features/login/impl/src/main/res/values-ko/translations.xml
@@ -39,7 +39,7 @@
"%1$s 에 로그인합니다"
"버전 %1$s"
"수동으로 로그인"
- "%1$s 에 로그인하세요."
+ "%1$s 에 로그인합니다"
"QR 코드로 로그인"
"계정 만들기"
"%1$s 에 오신 것을 환영합니다. 속도와 단순성을 극대화한 가장 빠른 버전입니다."
diff --git a/features/login/impl/src/main/res/values-nl/translations.xml b/features/login/impl/src/main/res/values-nl/translations.xml
index 03847a7ae9..8a7f695156 100644
--- a/features/login/impl/src/main/res/values-nl/translations.xml
+++ b/features/login/impl/src/main/res/values-nl/translations.xml
@@ -30,6 +30,7 @@
"Welkom terug!"
"Inloggen bij %1$s"
"Handmatig inloggen"
+ "Inloggen bij %1$s"
"Inloggen met QR-code"
"Account aanmaken"
"Welkom bij de snelste %1$s ooit. Supercharged, voor snelheid en eenvoud."
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 a2a7bdc688..c2aa0d5ed6 100644
--- a/features/login/impl/src/main/res/values-pt/translations.xml
+++ b/features/login/impl/src/main/res/values-pt/translations.xml
@@ -39,7 +39,7 @@
"Iniciar sessão em %1$s"
"Versão %1$s"
"Iniciar sessão manualmente"
- "Faz login em %1$s"
+ "Iniciar sessão em %1$s"
"Iniciar sessão com código QR"
"Criar conta"
"Bem-vindo(a) à %1$s mais rápida de sempre. Super rápida e simples."
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 f362537644..8ce58135aa 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,7 @@
"Другое"
"Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись."
"Сменить поставщика учетной записи"
+ "Google Play"
"Требуется приложение Element Pro для %1$s. Пожалуйста, загрузите его из магазина."
"Требуется Element Pro"
"Нам не удалось связаться с этим домашним сервером. Убедитесь, что вы правильно ввели URL-адрес домашнего сервера. Если URL-адрес указан правильно, обратитесь к администратору домашнего сервера за дополнительной помощью."
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 cfa7acf206..6d2bbeef13 100644
--- a/features/login/impl/src/main/res/values-tr/translations.xml
+++ b/features/login/impl/src/main/res/values-tr/translations.xml
@@ -30,6 +30,7 @@
"Tekrar hoş geldiniz!"
"%1$s adresinde oturum aç"
"Manuel olarak oturum aç"
+ "%1$s adresinde oturum aç"
"QR kodu ile giriş yap"
"Hesap oluştur"
"Şimdiye kadarki en hızlı %1$s hoş geldiniz. Hız ve basitlik için güçlendirildi."
diff --git a/features/login/impl/src/main/res/values-ur/translations.xml b/features/login/impl/src/main/res/values-ur/translations.xml
index 7b1216415e..334d5b6ea6 100644
--- a/features/login/impl/src/main/res/values-ur/translations.xml
+++ b/features/login/impl/src/main/res/values-ur/translations.xml
@@ -30,6 +30,7 @@
"واپس خوش آمدید!"
"%1$s میں داخل ہوں"
"دستی طور پر داخل ہوں"
+ "%1$s میں داخل ہوں"
"کیو آر (QR) رمز کیساتھ داخل ہوں"
"کھاتہ تخلیق کریں"
"اب تک کی تیز ترین %1$s میں خوش آمدید۔ رفتار اور سادگی کے لئے مشحون"
diff --git a/features/login/impl/src/main/res/values-uz/translations.xml b/features/login/impl/src/main/res/values-uz/translations.xml
index cbf065e925..373081f3c3 100644
--- a/features/login/impl/src/main/res/values-uz/translations.xml
+++ b/features/login/impl/src/main/res/values-uz/translations.xml
@@ -29,6 +29,7 @@
"Qaytib kelganingizdan xursandmiz!"
"Kirish%1$s"
"Qo\'lda tizimga kiring"
+ "Kirish%1$s"
"QR kod bilan tizimga kiring"
"Hisob yaratish"
"Eng tezkor %1$sga xush kelibsiz. Tezlik va oddylik uchun super zaryadlangan."
diff --git a/features/login/impl/src/main/res/values-zh-rTW/translations.xml b/features/login/impl/src/main/res/values-zh-rTW/translations.xml
index 23e290d69c..1b0b94d7a6 100644
--- a/features/login/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/login/impl/src/main/res/values-zh-rTW/translations.xml
@@ -39,7 +39,7 @@
"登入 %1$s"
"版本 %1$s"
"手動登入"
- "登入至 %1$s"
+ "登入 %1$s"
"使用 QR code 登入"
"建立帳號"
"歡迎使用有史以來最快的 %1$s。速度超快,操作簡便。"
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 b2f80e4ffe..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
@@ -117,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)
}
@@ -144,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)
}
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 17e8eb1dbd..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
@@ -29,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
@@ -79,10 +82,27 @@ 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(
@@ -236,6 +256,7 @@ private fun createPresenter(
rageshakeFeatureAvailability: () -> Flow = { flowOf(true) },
loginHelper: LoginHelper = createLoginHelper(),
onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null },
+ sessionStore: SessionStore = InMemorySessionStore(),
) = OnBoardingPresenter(
params = params,
buildMeta = buildMeta,
@@ -247,6 +268,7 @@ private fun createPresenter(
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
loginHelper = loginHelper,
onBoardingLogoResIdProvider = onBoardingLogoResIdProvider,
+ sessionStore = sessionStore,
)
fun createLoginHelper(
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/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 245643cb9d..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
@@ -14,13 +14,13 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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)
-@Inject
+@AssistedInject
class LogoutNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerStateProvider.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerStateProvider.kt
index e1bfee9a7f..b418b08726 100644
--- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerStateProvider.kt
+++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerStateProvider.kt
@@ -10,7 +10,7 @@ package io.element.android.features.messages.api.timeline.voicemessages.composer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.components.media.createFakeWaveform
import io.element.android.libraries.textcomposer.model.VoiceMessageState
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlin.time.Duration.Companion.seconds
open class VoiceMessageComposerStateProvider : PreviewParameterProvider {
@@ -42,4 +42,4 @@ fun aVoiceMessagePreviewState() = VoiceMessageState.Preview(
waveform = createFakeWaveform(),
)
-internal var aWaveformLevels = List(100) { it.toFloat() / 100 }.toPersistentList()
+internal var aWaveformLevels = List(100) { it.toFloat() / 100 }.toImmutableList()
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 5cbce99701..82aafcf545 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
@@ -20,7 +20,7 @@ import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.annotations.ContributesNode
import io.element.android.features.call.api.CallType
@@ -62,9 +62,9 @@ import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.dateformatter.api.toHumanReadableDuration
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.core.RoomId
+import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
@@ -73,6 +73,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.alias.matches
import io.element.android.libraries.matrix.api.room.joinedRoomMembers
+import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
@@ -91,11 +92,12 @@ import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class MessagesFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- private val matrixClient: MatrixClient,
+ private val roomListService: RoomListService,
+ private val sessionId: SessionId,
private val sendLocationEntryPoint: SendLocationEntryPoint,
private val showLocationEntryPoint: ShowLocationEntryPoint,
private val createPollEntryPoint: CreatePollEntryPoint,
@@ -194,7 +196,7 @@ class MessagesFlowNode(
}
.launchIn(lifecycleScope)
- matrixClient.roomListService
+ roomListService
.allRooms
.summaries
.onEach {
@@ -221,11 +223,13 @@ class MessagesFlowNode(
}
override fun onPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) {
- backstack.push(NavTarget.AttachmentPreview(
- attachment = attachments.first(),
- timelineMode = Timeline.Mode.Live,
- inReplyToEventId = inReplyToEventId,
- ))
+ backstack.push(
+ NavTarget.AttachmentPreview(
+ attachment = attachments.first(),
+ timelineMode = Timeline.Mode.Live,
+ inReplyToEventId = inReplyToEventId,
+ )
+ )
}
override fun onUserDataClick(userId: UserId) {
@@ -262,7 +266,7 @@ class MessagesFlowNode(
override fun onJoinCallClick(roomId: RoomId) {
val callType = CallType.RoomCall(
- sessionId = matrixClient.sessionId,
+ sessionId = sessionId,
roomId = roomId,
)
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
@@ -348,18 +352,20 @@ class MessagesFlowNode(
}
is NavTarget.CreatePoll -> {
createPollEntryPoint.nodeBuilder(this, buildContext)
- .params(CreatePollEntryPoint.Params(
- timelineMode = navTarget.timelineMode,
- mode = CreatePollMode.NewPoll
- ))
+ .params(
+ CreatePollEntryPoint.Params(
+ timelineMode = navTarget.timelineMode,
+ mode = CreatePollMode.NewPoll
+ )
+ )
.build()
}
is NavTarget.EditPoll -> {
createPollEntryPoint.nodeBuilder(this, buildContext)
.params(
CreatePollEntryPoint.Params(
- timelineMode = navTarget.timelineMode,
- mode = CreatePollMode.EditPoll(eventId = navTarget.eventId)
+ timelineMode = navTarget.timelineMode,
+ mode = CreatePollMode.EditPoll(eventId = navTarget.eventId)
)
)
.build()
@@ -412,11 +418,13 @@ class MessagesFlowNode(
}
override fun onPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) {
- backstack.push(NavTarget.AttachmentPreview(
- attachment = attachments.first(),
- timelineMode = Timeline.Mode.Thread(navTarget.threadRootId),
- inReplyToEventId = inReplyToEventId,
- ))
+ backstack.push(
+ NavTarget.AttachmentPreview(
+ attachment = attachments.first(),
+ timelineMode = Timeline.Mode.Thread(navTarget.threadRootId),
+ inReplyToEventId = inReplyToEventId,
+ )
+ )
}
override fun onUserDataClick(userId: UserId) {
@@ -453,12 +461,16 @@ class MessagesFlowNode(
override fun onJoinCallClick(roomId: RoomId) {
val callType = CallType.RoomCall(
- sessionId = matrixClient.sessionId,
+ sessionId = sessionId,
roomId = roomId,
)
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 250e271880..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
@@ -21,6 +21,6 @@ interface MessagesNavigator {
fun onReportContentClick(eventId: EventId, senderId: UserId)
fun onEditPollClick(eventId: EventId)
fun onPreviewAttachment(attachments: ImmutableList, inReplyToEventId: EventId?)
- fun onNavigateToRoom(roomId: RoomId, serverNames: List)
+ 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 1ab0885106..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
@@ -25,7 +25,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -74,7 +74,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class MessagesNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
@@ -92,6 +92,14 @@ class MessagesNode(
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,
@@ -99,18 +107,12 @@ class MessagesNode(
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, inReplyToEventId: EventId?)
fun onUserDataClick(userId: UserId)
@@ -122,9 +124,10 @@ class MessagesNode(
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() {
@@ -143,6 +146,14 @@ class MessagesNode(
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.
@@ -223,11 +234,11 @@ class MessagesNode(
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) }
}
}
@@ -236,10 +247,6 @@ class MessagesNode(
callbacks.forEach { it.onOpenThread(threadRootId, focusedEventId) }
}
- private fun onViewAllPinnedMessagesClick() {
- callbacks.forEach { it.onViewAllPinnedEvents() }
- }
-
private fun onSendLocationClick() {
callbacks.forEach { it.onSendLocationClick() }
}
@@ -252,10 +259,6 @@ class MessagesNode(
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)
}
@@ -291,7 +294,15 @@ class MessagesNode(
}
},
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 04a456d71b..5bf92ef6f6 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
@@ -24,7 +24,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.LifecycleResumeEffect
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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
@@ -57,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
@@ -70,6 +71,7 @@ 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
@@ -87,13 +89,13 @@ import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
-@Inject
+@AssistedInject
class MessagesPresenter(
@Assisted private val navigator: MessagesNavigator,
private val room: JoinedRoom,
@@ -121,6 +123,7 @@ class MessagesPresenter(
private val analyticsService: AnalyticsService,
private val encryptionService: EncryptionService,
private val featureFlagService: FeatureFlagService,
+ private val addRecentEmoji: AddRecentEmoji,
) : Presenter {
@AssistedFactory
interface Factory {
@@ -163,7 +166,7 @@ class MessagesPresenter(
derivedStateOf { roomInfo.avatarData() }
}
val heroes by remember {
- derivedStateOf { roomInfo.heroes().toPersistentList() }
+ derivedStateOf { roomInfo.heroes().toImmutableList() }
}
var hasDismissedInviteDialog by rememberSaveable {
@@ -282,8 +285,8 @@ class MessagesPresenter(
}
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 },
@@ -398,6 +401,7 @@ class MessagesPresenter(
) = 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 e20526d083..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
@@ -48,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
@@ -178,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/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt
index 20dc45ccc8..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
@@ -16,8 +16,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
-import dev.zacsweers.metro.Inject
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
@@ -43,6 +43,7 @@ import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.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
@@ -62,7 +63,7 @@ interface ActionListPresenter : Presenter {
}
}
-@Inject
+@AssistedInject
class DefaultActionListPresenter(
@Assisted
private val postProcessor: TimelineItemActionPostProcessor,
@@ -73,6 +74,7 @@ class DefaultActionListPresenter(
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
private val dateFormatter: DateFormatter,
private val featureFlagService: FeatureFlagService,
+ private val getRecentEmojis: GetRecentEmojis,
) : ActionListPresenter {
@AssistedFactory
@ContributesBinding(RoomScope::class)
@@ -153,14 +155,15 @@ class DefaultActionListPresenter(
),
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,
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..eeab4aa3a0 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,7 +23,8 @@ 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.toPersistentList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
open class ActionListStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -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(),
)
),
)
@@ -197,7 +209,7 @@ fun aTimelineItemActionList(
TimelineItemAction.ViewSource,
)
.sortedWith(TimelineItemActionComparator())
- .toPersistentList()
+ .toImmutableList()
}
fun aTimelineItemPollActionList(): ImmutableList {
@@ -210,5 +222,5 @@ fun aTimelineItemPollActionList(): ImmutableList {
TimelineItemAction.Redact,
)
.sortedWith(TimelineItemActionComparator())
- .toPersistentList()
+ .toImmutableList()
}
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 bb57cc82d4..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
@@ -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,
@@ -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 20697eea09..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
@@ -13,7 +13,7 @@ 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.Inject
+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
@@ -25,7 +25,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class AttachmentsPreviewNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 d33dbaf440..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
@@ -19,7 +19,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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
@@ -49,7 +49,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import timber.log.Timber
-@Inject
+@AssistedInject
class AttachmentsPreviewPresenter(
@Assisted private val attachment: Attachment,
@Assisted private val onDoneListener: OnDoneListener,
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 2b720882d2..b62b4116e6 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
@@ -16,8 +16,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
-import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
import io.element.android.libraries.di.SessionScope
@@ -29,12 +29,12 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.first
import timber.log.Timber
import kotlin.math.roundToLong
-@Inject
+@AssistedInject
class DefaultMediaOptimizationSelectorPresenter(
@Assisted private val localMedia: LocalMedia,
private val maxUploadSizeProvider: MaxUploadSizeProvider,
@@ -111,7 +111,7 @@ class DefaultMediaOptimizationSelectorPresenter(
canUpload = calculatedSize <= (maxUploadSize as AsyncData.Success).data
)
}
- .toPersistentList()
+ .toImmutableList()
.also { sizes ->
Timber.d(sizes.joinToString("\n") { "Calculated size for ${it.preset}: ${it.sizeInBytes} MB. Max upload size: $maxUploadSize" })
}
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 05287470ec..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
@@ -14,8 +14,8 @@ import android.util.Size
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 dev.zacsweers.metro.Inject
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.annotations.ApplicationContext
import kotlin.time.Duration
@@ -30,7 +30,7 @@ interface VideoMetadataExtractor : AutoCloseable {
}
@ContributesBinding(AppScope::class)
-@Inject
+@AssistedInject
class DefaultVideoMetadataExtractor(
@ApplicationContext private val context: Context,
@Assisted private val uri: Uri,
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 d03d034665..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
@@ -18,7 +18,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -31,7 +31,7 @@ import io.element.android.libraries.roomselect.api.RoomSelectMode
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class ForwardMessagesNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 a52a5ca2cd..3e3860db3e 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
@@ -12,7 +12,7 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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
@@ -22,11 +22,11 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import io.element.android.libraries.matrix.api.timeline.getActiveTimeline
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-@Inject
+@AssistedInject
class ForwardMessagesPresenter(
@Assisted eventId: String,
@Assisted private val timelineProvider: TimelineProvider,
@@ -43,7 +43,7 @@ class ForwardMessagesPresenter(
private val forwardingActionState: MutableState>> = mutableStateOf(AsyncAction.Uninitialized)
fun onRoomSelected(roomIds: List) {
- sessionCoroutineScope.forwardEvent(eventId, roomIds.toPersistentList(), forwardingActionState)
+ sessionCoroutineScope.forwardEvent(eventId, roomIds.toImmutableList(), forwardingActionState)
}
@Composable
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 36d85a5580..9505f0d758 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
@@ -26,7 +26,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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
@@ -79,7 +79,7 @@ import io.element.android.services.analyticsproviders.api.trackers.captureIntera
import io.element.android.wysiwyg.compose.RichTextEditorState
import io.element.android.wysiwyg.display.TextDisplay
import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
@@ -97,7 +97,7 @@ import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes
-@Inject
+@AssistedInject
class MessageComposerPresenter(
@Assisted private val navigator: MessagesNavigator,
@Assisted private val timelineController: TimelineController,
@@ -379,7 +379,7 @@ class MessageComposerPresenter(
showAttachmentSourcePicker = showAttachmentSourcePicker,
showTextFormatting = showTextFormatting,
canShareLocation = canShareLocation.value,
- suggestions = suggestions.toPersistentList(),
+ suggestions = suggestions.toImmutableList(),
resolveMentionDisplay = resolveMentionDisplay,
resolveAtRoomMentionDisplay = resolveAtRoomMentionDisplay,
eventSink = { handleEvents(it) },
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 45898810cb..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
@@ -19,7 +19,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -38,7 +38,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class PinnedMessagesListNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 72389c891b..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
@@ -19,7 +19,7 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
-@Inject
+@AssistedInject
class PinnedMessagesListPresenter(
@Assisted private val navigator: PinnedMessagesListNavigator,
private val room: JoinedRoom,
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 76d2d833ef..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
@@ -13,7 +13,7 @@ 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.Inject
+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
@@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class ReportMessageNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 6bde64f3ad..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
@@ -17,7 +17,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-@Inject
+@AssistedInject
class ReportMessagePresenter(
private val room: JoinedRoom,
@Assisted private val inputs: Inputs,
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 6f49b5b0f8..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
@@ -25,7 +24,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -44,19 +43,18 @@ 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.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
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,19 +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)
-@Inject
+@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,
@@ -125,6 +122,7 @@ class ThreadedMessagesNode(
fun onCreatePollClick()
fun onEditPollClick(eventId: EventId)
fun onJoinCallClick(roomId: RoomId)
+ fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?)
}
override fun onBuilt() {
@@ -191,8 +189,11 @@ class ThreadedMessagesNode(
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) }
@@ -219,7 +220,14 @@ class ThreadedMessagesNode(
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() }
@@ -233,13 +241,6 @@ class ThreadedMessagesNode(
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)
@@ -273,11 +274,11 @@ class ThreadedMessagesNode(
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/TimelineController.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt
index a0f4c2ce0e..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
@@ -14,6 +14,7 @@ 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.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
@@ -74,21 +75,26 @@ class TimelineController(
}
}
- 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
+ }
+ }
}
/**
@@ -136,3 +142,8 @@ class TimelineController(
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/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
index e3954e62a2..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
@@ -22,7 +22,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import timber.log.Timber
const val FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS = 200L
-@Inject
+@AssistedInject
class TimelinePresenter(
timelineItemsFactoryCreator: TimelineItemsFactory.Creator,
private val room: JoinedRoom,
@@ -117,8 +118,8 @@ class TimelinePresenter(
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) }
@@ -207,7 +208,7 @@ class TimelinePresenter(
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(
@@ -257,13 +258,39 @@ class TimelinePresenter(
}
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
}
@@ -341,7 +368,7 @@ class TimelinePresenter(
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 e7dc71f185..0cc61b4e4c 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
@@ -41,7 +41,6 @@ import io.element.android.libraries.matrix.ui.messages.reply.aProfileTimelineDet
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
-import kotlinx.collections.immutable.toPersistentList
import java.util.UUID
import kotlin.random.Random
@@ -197,7 +196,7 @@ fun aTimelineItemReactions(
)
)
}
- }.toPersistentList()
+ }.toImmutableList()
)
}
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/TimelineItemStateEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt
index 2f66d586a9..84fbd2920a 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt
@@ -33,7 +33,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.util.defaultTimelineContentPadding
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.persistentListOf
@Composable
fun TimelineItemStateEventRow(
@@ -100,7 +100,7 @@ internal fun TimelineItemStateEventRowPreview() = ElementPreview {
content = aTimelineItemStateEventContent(),
groupPosition = TimelineItemGroupPosition.None,
readReceiptState = TimelineItemReadReceipts(
- receipts = listOf(aReadReceiptData(0)).toPersistentList(),
+ receipts = persistentListOf(aReadReceiptData(0)),
)
),
renderReadReceipts = true,
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 23abe8ea74..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
@@ -17,6 +17,7 @@ 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
@@ -50,7 +51,13 @@ fun CustomReactionBottomSheet(
sheetState = sheetState,
modifier = modifier
) {
- val presenter = remember { EmojiPickerPresenter(target.emojibaseStore) }
+ val presenter = remember {
+ EmojiPickerPresenter(
+ emojibaseStore = target.emojibaseStore,
+ recentEmojis = state.recentEmojis,
+ coroutineDispatchers = CoroutineDispatchers.Default,
+ )
+ }
EmojiPicker(
onSelectEmoji = ::onEmojiSelectedDismiss,
state = presenter.present(),
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 b7d674d1de..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,29 +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
@Inject
class CustomReactionPresenter(
- private val emojibaseProvider: EmojibaseProvider
+ 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
@@ -56,9 +66,11 @@ class CustomReactionPresenter(
?.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/picker/EmojiPicker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt
index 2bf6afafb5..83b21df092 100644
--- 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 New Vector Ltd.
+ * Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
@@ -30,16 +30,16 @@ 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.emojibasebindings.EmojibaseCategory
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.features.messages.impl.timeline.components.customreaction.title
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
@@ -53,9 +53,7 @@ fun EmojiPicker(
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
- val categories = state.categories
- val pagerState = rememberPagerState(pageCount = { EmojibaseCategory.entries.size })
-
+ val pagerState = rememberPagerState(pageCount = { state.categories.size })
Column(modifier) {
SearchBar(
modifier = Modifier.padding(bottom = 10.dp),
@@ -66,36 +64,31 @@ fun EmojiPicker(
onActiveChange = { state.eventSink(EmojiPickerEvents.ToggleSearchActive(it)) },
windowInsets = WindowInsets(0, 0, 0, 0),
placeHolderTitle = stringResource(CommonStrings.emoji_picker_search_placeholder),
- ) { results ->
- val emojis = results
- 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 ->
- SelectableEmojiItem(
- item = item,
- selectedEmojis = selectedEmojis,
- onSelectEmoji = onSelectEmoji,
- )
- }
- }
+ ) { emojis ->
+ EmojiResults(
+ emojis = emojis,
+ isEmojiSelected = { selectedEmojis.contains(it.unicode) },
+ onSelectEmoji = onSelectEmoji,
+ )
}
if (!state.isSearchActive) {
SecondaryTabRow(
selectedTabIndex = pagerState.currentPage,
) {
- EmojibaseCategory.entries.forEachIndexed { index, category ->
+ state.categories.forEachIndexed { index, category ->
Tab(
icon = {
- Icon(
- imageVector = category.icon,
- contentDescription = stringResource(id = category.title)
- )
+ 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 = {
@@ -109,41 +102,40 @@ fun EmojiPicker(
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 ->
- SelectableEmojiItem(
- item = item,
- selectedEmojis = selectedEmojis,
- onSelectEmoji = onSelectEmoji,
- )
- }
- }
+ val emojis = state.categories[index].emojis
+ EmojiResults(
+ emojis = emojis,
+ isEmojiSelected = { selectedEmojis.contains(it.unicode) },
+ onSelectEmoji = onSelectEmoji,
+ )
}
}
}
}
@Composable
-private fun SelectableEmojiItem(
- item: Emoji,
- selectedEmojis: ImmutableSet,
+private fun EmojiResults(
+ emojis: ImmutableList,
+ isEmojiSelected: (Emoji) -> Boolean,
onSelectEmoji: (Emoji) -> Unit,
) {
- EmojiItem(
- modifier = Modifier.aspectRatio(1f),
- item = item,
- isSelected = selectedEmojis.contains(item.unicode),
- onSelectEmoji = onSelectEmoji,
- emojiSize = 32.dp.toSp(),
- )
+ 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
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
index de5b9f17a5..ce9600b1f7 100644
--- 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
@@ -14,26 +14,57 @@ 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.Dispatchers
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 categories = remember { emojibaseStore.categories }
+
+ 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()) {
@@ -43,7 +74,7 @@ class EmojiPickerPresenter(
delay(100.milliseconds)
val lowercaseQuery = searchQuery.lowercase()
- val results = withContext(Dispatchers.Default) {
+ val results = withContext(coroutineDispatchers.computation) {
emojibaseStore.allEmojis
.asSequence()
.filter { emoji ->
@@ -71,6 +102,7 @@ class EmojiPickerPresenter(
return EmojiPickerState(
categories = categories,
+ allEmojis = emojibaseStore.allEmojis,
searchQuery = searchQuery,
isSearchActive = isSearchActive,
searchResults = emojiResults,
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
index 761f2f5bcd..595349a503 100644
--- 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
@@ -7,16 +7,26 @@
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.emojibasebindings.EmojibaseCategory
+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.ImmutableMap
data class EmojiPickerState(
- val categories: ImmutableMap>,
+ 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
index 5cff7cf9e0..f248efe893 100644
--- 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
@@ -10,11 +10,15 @@ package io.element.android.features.messages.impl.timeline.components.customreac
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.ImmutableMap
import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toImmutableMap
+import kotlinx.collections.immutable.toImmutableList
class EmojiPickerStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -25,57 +29,52 @@ class EmojiPickerStateProvider : PreviewParameterProvider {
anEmojiPickerState(
isSearchActive = true,
searchQuery = "smile",
- searchResults = SearchBarResultState.Results(
- persistentListOf(
- Emoji(
- "0x00",
- "grinning face",
- persistentListOf("grinning"),
- persistentListOf("smile, grin"),
- "😀",
- null
- ),
- Emoji(
- "0x01",
- "crying face",
- persistentListOf("crying"),
- persistentListOf("smile, crying"),
- "\uD83E\uDD72",
- null
- ),
- )
- )
+ 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: ImmutableMap> = EmojibaseCategory.entries.associateWith {
- persistentListOf(
- Emoji(
- "0x00",
- "grinning face",
- persistentListOf("grinning"),
- persistentListOf("smile, grin"),
- "😀",
- null
- ),
- Emoji(
- "0x01",
- "crying face",
- persistentListOf("crying"),
- persistentListOf("smile, crying"),
- "\uD83E\uDD72",
- null
- ),
+ categories: ImmutableList = (listOf(recentEmojisCategory()) + EmojibaseCategory.entries.map {
+ EmojiCategory(
+ titleId = it.title,
+ icon = IconSource.Vector(it.icon),
+ emojis = emojiList(),
)
- }.toImmutableMap(),
+ }).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,
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 7cdf4acc00..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
@@ -13,7 +13,7 @@ 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.Inject
+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
@@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class EventDebugInfoNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 a040b49e69..029373d42b 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
@@ -9,7 +9,7 @@ package io.element.android.features.messages.impl.timeline.factories
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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
@@ -21,7 +21,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -29,7 +29,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
-@Inject
+@AssistedInject
class TimelineItemsFactory(
@Assisted config: TimelineItemsFactoryConfig,
eventItemFactoryCreator: TimelineItemEventFactory.Creator,
@@ -94,7 +94,7 @@ class TimelineItemsFactory(
newTimelineItemStates.add(updatedItem)
}
}
- val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()
+ val result = timelineItemGrouper.group(newTimelineItemStates).toImmutableList()
this._timelineItems.emit(result)
}
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 8047acd09a..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
@@ -13,6 +13,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
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
@@ -31,7 +32,6 @@ 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 io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
@Inject
class TimelineItemContentFactory(
@@ -45,7 +45,7 @@ class TimelineItemContentFactory(
private val stateFactory: TimelineItemContentStateFactory,
private val failedToParseMessageFactory: TimelineItemContentFailedToParseMessageFactory,
private val failedToParseStateFactory: TimelineItemContentFailedToParseStateFactory,
- private val currentSessionIdHolder: CurrentSessionIdHolder,
+ private val sessionId: SessionId,
) {
suspend fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
return create(
@@ -64,7 +64,7 @@ class TimelineItemContentFactory(
sender: UserId,
senderProfile: ProfileTimelineDetails,
): TimelineItemEventContent {
- val isOutgoing = currentSessionIdHolder.current == sender
+ val isOutgoing = sessionId == sender
return when (itemContent) {
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
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 d3b3c5d679..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
@@ -9,7 +9,7 @@ package io.element.android.features.messages.impl.timeline.factories.event
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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
@@ -39,7 +39,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.util.Date
-@Inject
+@AssistedInject
class TimelineItemEventFactory(
@Assisted private val config: TimelineItemsFactoryConfig,
private val contentFactory: TimelineItemContentFactory,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReactions.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReactions.kt
index 84428790bc..115d8dab56 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReactions.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReactions.kt
@@ -8,7 +8,7 @@
package io.element.android.features.messages.impl.timeline.model
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
data class TimelineItemReactions(
val reactions: ImmutableList
@@ -17,5 +17,5 @@ data class TimelineItemReactions(
get() = reactions
.filter { it.isHighlighted }
.map { it.key }
- .toPersistentList()
+ .toImmutableList()
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReactionsProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReactionsProvider.kt
index c1f64efff4..b0084f20ee 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReactionsProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReactionsProvider.kt
@@ -7,9 +7,9 @@
package io.element.android.features.messages.impl.timeline.model
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
fun aTimelineItemReactions() = TimelineItemReactions(
// Use values from AggregatedReactionProvider
- reactions = AggregatedReactionProvider().values.toPersistentList()
+ reactions = AggregatedReactionProvider().values.toImmutableList()
)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt
index 37c9a906bf..211bc5037b 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt
@@ -11,7 +11,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaSource
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
@@ -53,7 +53,7 @@ fun aTimelineItemVoiceContent(
duration = duration,
mediaSource = mediaSource,
mimeType = mimeType,
- waveform = waveform.toPersistentList(),
+ waveform = waveform.toImmutableList(),
formattedFileSize = "1.0 MB",
fileExtension = "ogg",
)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/MutableListExt.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/MutableListExt.kt
deleted file mode 100644
index b59acde997..0000000000
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/MutableListExt.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright 2022-2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.messages.impl.timeline.util
-
-internal inline fun MutableList.invalidateLast() {
- val indexOfLast = size
- if (indexOfLast > 0) {
- set(indexOfLast - 1, null)
- }
-}
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 c8c33c52b2..7c5eeb4969 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
@@ -21,8 +21,8 @@ import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
-import dev.zacsweers.metro.Inject
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
@@ -43,7 +43,6 @@ import io.element.android.libraries.voicerecorder.api.VoiceRecorderState
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
-import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -51,7 +50,7 @@ import java.io.File
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
-@Inject
+@AssistedInject
class DefaultVoiceMessageComposerPresenter(
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
@Assisted private val timelineMode: Timeline.Mode,
@@ -199,7 +198,7 @@ class DefaultVoiceMessageComposerPresenter(
voiceMessageState = when (val state = recorderState) {
is VoiceRecorderState.Recording -> VoiceMessageState.Recording(
duration = state.elapsedTime,
- levels = state.levels.toPersistentList(),
+ levels = state.levels.toImmutableList(),
)
is VoiceRecorderState.Finished ->
previewState(
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 5d9f4edfb5..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
@@ -10,10 +10,10 @@ package io.element.android.features.messages.impl.voicemessages.timeline
import androidx.compose.runtime.Composable
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.Inject
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
@@ -32,7 +32,7 @@ interface VoiceMessagePresenterModule {
fun bindVoiceMessagePresenterFactory(factory: VoiceMessagePresenter.Factory): TimelineItemPresenterFactory<*, *>
}
-@Inject
+@AssistedInject
class VoiceMessagePresenter(
voiceMessagePresenterFactory: VoiceMessagePresenterFactory,
@Assisted private val content: TimelineItemVoiceContent,
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-cy/translations.xml b/features/messages/impl/src/main/res/values-cy/translations.xml
index 5591e3abd7..2d2c368fa3 100644
--- a/features/messages/impl/src/main/res/values-cy/translations.xml
+++ b/features/messages/impl/src/main/res/values-cy/translations.xml
@@ -54,6 +54,14 @@
- "Ymatebodd %1$d aelod gyda %2$s"
- "Ymatebodd %1$d aelod gyda %2$s"
+
+ - "Fe wnaethoch chi a%1$d aelod ymateb gyda%2$s"
+ - "Fe wnaethoch chi a%1$d aelod ymateb gyda%2$s"
+ - "Fe wnaethoch chi a%1$d aelod ymateb gyda%2$s"
+ - "Fe wnaethoch chi a%1$d aelod ymateb gyda%2$s"
+ - "Fe wnaethoch chi a%1$d aelod ymateb gyda%2$s"
+ - "Fe wnaethoch chi a%1$d aelod ymateb 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 f8db7fcafa..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,6 +7,7 @@
"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"
@@ -15,6 +16,7 @@
"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"
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 3e1af3ce01..26c2c7fd65 100644
--- a/features/messages/impl/src/main/res/values-fi/translations.xml
+++ b/features/messages/impl/src/main/res/values-fi/translations.xml
@@ -16,6 +16,7 @@
"Median lähettäminen epäonnistui, yritä uudelleen."
"Suurin sallittu tiedostokoko on %1$s."
"Tiedosto on liian suuri lähetettäväksi"
+ "Kohde %1$d / %2$d"
"Optimoi kuvanlaatu"
"Käsitellään…"
"Estä käyttäjä"
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-nb/translations.xml b/features/messages/impl/src/main/res/values-nb/translations.xml
index deee637c09..5df3e5eac3 100644
--- a/features/messages/impl/src/main/res/values-nb/translations.xml
+++ b/features/messages/impl/src/main/res/values-nb/translations.xml
@@ -7,13 +7,17 @@
"Gjenstander"
"Smilefjes og mennesker"
"Reising og steder"
+ "Nylige emojier"
"Symboler"
"Teksting er kanskje ikke synlig for personer som bruker eldre apper."
+ "Trykk for å endre kvaliteten på videoopplastingen"
"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"
+ "Optimaliser bildekvaliteten"
+ "Behandler…"
"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/translations.xml b/features/messages/impl/src/main/res/values-pt/translations.xml
index 619f69e2da..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,6 +7,7 @@
"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"
@@ -15,6 +16,7 @@
"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"
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 da8344f18a..a121bc7843 100644
--- a/features/messages/impl/src/main/res/values-ro/translations.xml
+++ b/features/messages/impl/src/main/res/values-ro/translations.xml
@@ -7,6 +7,7 @@
"Obiecte"
"Fețe zâmbitoare & Oameni"
"Călătorii & Locuri"
+ "Emoticoane recente"
"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"
@@ -15,6 +16,7 @@
"Î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."
+ "Elementul %1$d din %2$d"
"Optimizați calitatea imaginii"
"Se procesează…"
"Blocați utilizatorul"
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 0cc79fba16..4656a5978b 100644
--- a/features/messages/impl/src/main/res/values-ru/translations.xml
+++ b/features/messages/impl/src/main/res/values-ru/translations.xml
@@ -7,10 +7,18 @@
"Объекты"
"Улыбки и люди"
"Путешествия и места"
+ "Недавние эмодзи"
"Символы"
"Подпись может быть не видна пользователям старых приложений."
+ "Нажмите, чтобы изменить качество загружаемого видео."
+ "Файл не может быть загружен."
"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."
"Не удалось загрузить медиафайлы, попробуйте еще раз."
+ "Максимальный размер файла: %1$s."
+ "Файл слишком большой для загрузки."
+ "Элемент %1$d из %2$d"
+ "Оптимизировать качество изображения"
+ "Обработка…"
"Заблокировать пользователя"
"Отметьте, хотите ли вы скрыть все текущие и будущие сообщения от этого пользователя"
"Это сообщение будет передано администратору вашего домашнего сервера. Они не смогут прочитать зашифрованные сообщения."
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 7bbab91d45..8d43d73632 100644
--- a/features/messages/impl/src/main/res/values-zh/translations.xml
+++ b/features/messages/impl/src/main/res/values-zh/translations.xml
@@ -7,6 +7,7 @@
"物品"
"表情和人物"
"旅行和地点"
+ "最近的 Emoji"
"符号"
"使用旧版应用程序的用户可能无法看到字幕。"
"点按以更改视频上传质量"
@@ -15,6 +16,7 @@
"上传媒体失败,请重试。"
"允许的最大文件大小为%1$s 。"
"文件太大,无法上传"
+ "第%1$d/%2$d项"
"优化图像质量"
"处理中…"
"封禁用户"
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt
index a4753807cd..2de761ad69 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt
@@ -30,9 +30,10 @@ 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_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID
-import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
+import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
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
@@ -60,7 +61,8 @@ class DefaultMessagesEntryPointTest {
MessagesFlowNode(
buildContext = buildContext,
plugins = plugins,
- matrixClient = FakeMatrixClient(),
+ roomListService = FakeRoomListService(),
+ sessionId = A_SESSION_ID,
sendLocationEntryPoint = object : SendLocationEntryPoint {
override fun builder(timelineMode: Timeline.Mode) = lambdaError()
},
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 7320671694..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
@@ -22,7 +22,7 @@ class FakeMessagesNavigator(
private val onReportContentClickLambda: (eventId: EventId, senderId: UserId) -> Unit = { _, _ -> lambdaError() },
private val onEditPollClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() },
private val onPreviewAttachmentLambda: (attachments: ImmutableList, inReplyToEventId: EventId?) -> Unit = { _, _ -> lambdaError() },
- private val onNavigateToRoomLambda: (roomId: RoomId, serverNames: List) -> 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) {
@@ -45,8 +45,8 @@ class FakeMessagesNavigator(
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 52c5ee2259..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
@@ -57,6 +57,7 @@ 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
@@ -75,6 +76,7 @@ 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
@@ -794,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()
}
},
@@ -820,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()
}
},
@@ -1269,6 +1271,7 @@ class MessagesPresenterTest {
encryptionService: FakeEncryptionService = FakeEncryptionService(),
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
actionListEventSink: (ActionListEvents) -> Unit = {},
+ addRecentEmoji: AddRecentEmoji = AddRecentEmoji(FakeMatrixClient(), testCoroutineDispatchers()),
): MessagesPresenter {
return MessagesPresenter(
room = joinedRoom,
@@ -1297,6 +1300,7 @@ class MessagesPresenterTest {
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 c716b986b6..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
@@ -370,6 +370,7 @@ class MessagesViewTest {
displayEmojiReactions = true,
actions = persistentListOf(TimelineItemAction.Edit),
verifiedUserSendFailure = VerifiedUserSendFailure.None,
+ recentEmojis = persistentListOf(),
)
),
)
@@ -462,6 +463,7 @@ class MessagesViewTest {
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(TimelineItemAction.Edit),
+ recentEmojis = persistentListOf(),
),
),
customReactionState = aCustomReactionState(
@@ -491,6 +493,7 @@ class MessagesViewTest {
displayEmojiReactions = true,
verifiedUserSendFailure = aChangedIdentitySendFailure(),
actions = persistentListOf(),
+ recentEmojis = persistentListOf(),
),
),
timelineState = aTimelineState(eventSink = eventsRecorder)
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 e8766798cd..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
@@ -94,7 +94,8 @@ class ActionListPresenterTest {
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.ViewSource,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -135,7 +136,8 @@ class ActionListPresenterTest {
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.ViewSource,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -182,7 +184,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -228,7 +231,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -274,7 +278,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -322,7 +327,8 @@ class ActionListPresenterTest {
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -370,7 +376,8 @@ class ActionListPresenterTest {
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -417,7 +424,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -463,7 +471,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -509,7 +518,8 @@ class ActionListPresenterTest {
TimelineItemAction.Pin,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -552,7 +562,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyLink,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -599,7 +610,8 @@ class ActionListPresenterTest {
TimelineItemAction.Pin,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -650,7 +662,8 @@ class ActionListPresenterTest {
TimelineItemAction.RemoveCaption,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -699,7 +712,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyCaption,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -739,7 +753,8 @@ class ActionListPresenterTest {
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.ViewSource,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -812,7 +827,8 @@ class ActionListPresenterTest {
TimelineItemAction.Pin,
TimelineItemAction.CopyText,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -858,7 +874,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -911,7 +928,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
@@ -1004,7 +1022,8 @@ class ActionListPresenterTest {
TimelineItemAction.Edit,
TimelineItemAction.CopyText,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
}
@@ -1048,7 +1067,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyLink,
TimelineItemAction.Pin,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
}
@@ -1091,7 +1111,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyLink,
TimelineItemAction.Pin,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
}
@@ -1133,7 +1154,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyLink,
TimelineItemAction.Pin,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
}
@@ -1178,7 +1200,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyLink,
TimelineItemAction.Pin,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
}
@@ -1215,7 +1238,8 @@ class ActionListPresenterTest {
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.ViewSource
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
}
@@ -1292,7 +1316,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyLink,
TimelineItemAction.Pin,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
}
@@ -1345,7 +1370,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyLink,
TimelineItemAction.Pin,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
}
@@ -1399,7 +1425,8 @@ class ActionListPresenterTest {
TimelineItemAction.CopyLink,
TimelineItemAction.Pin,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
}
@@ -1450,7 +1477,8 @@ class ActionListPresenterTest {
// Can't reply in thread for local events
TimelineItemAction.Reply,
TimelineItemAction.Redact,
- )
+ ),
+ recentEmojis = persistentListOf(),
)
)
}
@@ -1472,5 +1500,6 @@ private fun createActionListPresenter(
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/fixtures/TimelineItemsFactoryFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt
index 502ab74062..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
@@ -33,7 +33,6 @@ import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
-import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
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
@@ -78,7 +77,7 @@ internal fun TestScope.aTimelineItemsFactory(
stateFactory = TimelineItemContentStateFactory(timelineEventFormatter),
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(),
- currentSessionIdHolder = CurrentSessionIdHolder(matrixClient),
+ sessionId = matrixClient.sessionId,
),
matrixClient = matrixClient,
dateFormatter = FakeDateFormatter(),
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 71d295ba8b..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)
}
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/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt
index 73969a2860..451f5bddcc 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt
@@ -36,7 +36,7 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.setSafeContent
import io.element.android.wysiwyg.link.Link
import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
@@ -148,7 +148,7 @@ class TimelineViewTest {
eventId = EventId("\$event_$it"),
content = aTimelineItemUnknownContent(),
)
- }.toPersistentList()
+ }.toImmutableList()
rule.setTimelineView(
state = aTimelineState(
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/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..cf4930b389
--- /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.toImmutableMap
+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().toImmutableMap()),
+ 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/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
index 6040032ebc..0d6a96d75d 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
@@ -46,7 +46,6 @@ import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.collections.immutable.toImmutableList
-import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
@@ -85,7 +84,7 @@ class VoiceMessageComposerPresenterTest {
companion object {
private val RECORDING_DURATION = 1.seconds
- private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, listOf(0.1f, 0.2f).toPersistentList())
+ private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, listOf(0.1f, 0.2f).toImmutableList())
}
@Test
diff --git a/features/migration/impl/build.gradle.kts b/features/migration/impl/build.gradle.kts
index dd445624b9..8926f258bd 100644
--- a/features/migration/impl/build.gradle.kts
+++ b/features/migration/impl/build.gradle.kts
@@ -19,6 +19,7 @@ android {
setupDependencyInjection()
dependencies {
+ implementation(projects.features.announcement.api)
implementation(projects.features.migration.api)
implementation(projects.libraries.architecture)
implementation(projects.libraries.androidutils)
@@ -34,5 +35,6 @@ dependencies {
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.libraries.preferences.test)
+ testImplementation(projects.features.announcement.test)
testImplementation(projects.features.rageshake.test)
}
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 7edbc5389f..d4e6d6dc37 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
@@ -31,6 +31,7 @@ class MigrationPresenter(
) : Presenter {
private val orderedMigrations = migrations.sortedBy { it.order }
private val lastMigration: Int = orderedMigrations.lastOrNull()?.order ?: 0
+ private var isFreshInstall = false
@Composable
override fun present(): MigrationState {
@@ -49,6 +50,7 @@ class MigrationPresenter(
val migrationValue = migrationStoreVersion ?: return@LaunchedEffect
if (migrationValue == -1) {
Timber.d("Fresh install, or previous installed application did not have the migration mechanism.")
+ isFreshInstall = true
}
if (migrationValue == lastMigration) {
Timber.d("Current app migration version: $migrationValue. No migration needed.")
@@ -59,7 +61,7 @@ class MigrationPresenter(
val nextMigration = orderedMigrations.firstOrNull { it.order > migrationValue }
if (nextMigration != null) {
Timber.d("Current app migration version: $migrationValue. Applying migration: ${nextMigration.order}")
- nextMigration.migrate()
+ nextMigration.migrate(isFreshInstall)
migrationStore.setApplicationMigrationVersion(nextMigration.order)
}
}
diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration.kt
index a0327e8b09..f14ec89dbe 100644
--- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration.kt
+++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration.kt
@@ -9,5 +9,5 @@ package io.element.android.features.migration.impl.migrations
interface AppMigration {
val order: Int
- suspend fun migrate()
+ suspend fun migrate(isFreshInstall: Boolean)
}
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 048a400f6c..88fdb16b9e 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
@@ -22,7 +22,7 @@ class AppMigration01(
) : AppMigration {
override val order: Int = 1
- override suspend fun migrate() {
+ override suspend fun migrate(isFreshInstall: Boolean) {
logFilesRemover.perform()
}
}
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 44f4806c65..0ba2712427 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
@@ -27,7 +27,7 @@ class AppMigration02(
) : AppMigration {
override val order: Int = 2
- override suspend fun migrate() {
+ override suspend fun migrate(isFreshInstall: Boolean) {
coroutineScope {
for (session in sessionStore.getAllSessions()) {
val sessionId = SessionId(session.userId)
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 0cb3573954..e24e18a205 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
@@ -21,7 +21,7 @@ class AppMigration03(
) : AppMigration {
override val order: Int = 3
- override suspend fun migrate() {
- migration01.migrate()
+ override suspend fun migrate(isFreshInstall: Boolean) {
+ migration01.migrate(isFreshInstall)
}
}
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 8ab4921038..f1f16b7a97 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
@@ -27,7 +27,7 @@ class AppMigration04(
}
override val order: Int = 4
- override suspend fun migrate() {
+ override suspend fun migrate(isFreshInstall: Boolean) {
runCatchingExceptions { context.getDatabasePath(NOTIFICATION_FILE_NAME).delete() }
}
}
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 109ff7e0b7..2a60822c5d 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
@@ -22,7 +22,7 @@ class AppMigration05(
) : AppMigration {
override val order: Int = 5
- override suspend fun migrate() {
+ override suspend fun migrate(isFreshInstall: Boolean) {
val allSessions = sessionStore.getAllSessions()
for (session in allSessions) {
if (session.sessionPath.isEmpty()) {
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 2eb98b9e5f..be3050f919 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
@@ -25,7 +25,7 @@ class AppMigration06(
) : AppMigration {
override val order: Int = 6
- override suspend fun migrate() {
+ override suspend fun migrate(isFreshInstall: Boolean) {
val allSessions = sessionStore.getAllSessions()
for (session in allSessions) {
if (session.cachePath.isEmpty()) {
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 fe88817796..5367323b75 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
@@ -22,7 +22,7 @@ class AppMigration07(
) : AppMigration {
override val order: Int = 7
- override suspend fun migrate() {
+ override suspend fun migrate(isFreshInstall: Boolean) {
logFilesRemover.perform { file ->
file.name.startsWith("logs-")
}
diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration08.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration08.kt
new file mode 100644
index 0000000000..12355b4bc4
--- /dev/null
+++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration08.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.migration.impl.migrations
+
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesIntoSet
+import dev.zacsweers.metro.Inject
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.api.AnnouncementService
+
+/**
+ * Ensure the new notification sound banner is displayed, but only on application upgrade.
+ */
+@ContributesIntoSet(AppScope::class)
+@Inject
+class AppMigration08(
+ private val announcementService: AnnouncementService,
+) : AppMigration {
+ override val order: Int = 8
+
+ override suspend fun migrate(isFreshInstall: Boolean) {
+ if (!isFreshInstall) {
+ announcementService.showAnnouncement(Announcement.NewNotificationSound)
+ }
+ }
+}
diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/MigrationPresenterTest.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/MigrationPresenterTest.kt
index 635a06f028..e5898ea0dd 100644
--- a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/MigrationPresenterTest.kt
+++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/MigrationPresenterTest.kt
@@ -15,8 +15,10 @@ import io.element.android.features.migration.impl.migrations.AppMigration
import io.element.android.libraries.architecture.AsyncData
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate
-import io.element.android.tests.testutils.lambda.LambdaNoParamRecorder
+import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
+import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@@ -48,13 +50,18 @@ class MigrationPresenterTest {
assertThat(store.applicationMigrationVersion().first()).isEqualTo(migrations.maxOf { it.order })
}
for (migration in migrations) {
- migration.migrateLambda.assertions().isCalledOnce()
+ migration.migrateLambda.assertions().isCalledOnce().with(value(true))
}
}
@Test
fun `present - no migration should occurs if ApplicationMigrationVersion is the last one`() = runTest {
- val migrations = (1..10).map { FakeAppMigration(it) }
+ val migrations = (1..10).map {
+ FakeAppMigration(
+ order = it,
+ migrateLambda = lambdaRecorder { lambdaError() },
+ )
+ }
val store = InMemoryMigrationStore(migrations.maxOf { it.order })
val presenter = createPresenter(
migrationStore = store,
@@ -90,7 +97,7 @@ class MigrationPresenterTest {
consumeItemsUntilPredicate { it.migrationAction is AsyncData.Success }
assertThat(store.applicationMigrationVersion().first()).isEqualTo(migrations.maxOf { it.order })
for (migration in migrations) {
- migration.migrateLambda.assertions().isCalledOnce()
+ migration.migrateLambda.assertions().isCalledOnce().with(value(false))
}
}
}
@@ -106,9 +113,9 @@ private fun createPresenter(
private class FakeAppMigration(
override val order: Int,
- val migrateLambda: LambdaNoParamRecorder = lambdaRecorder { -> },
+ val migrateLambda: LambdaOneParamRecorder = lambdaRecorder { },
) : AppMigration {
- override suspend fun migrate() {
- migrateLambda()
+ override suspend fun migrate(isFreshInstall: Boolean) {
+ migrateLambda(isFreshInstall)
}
}
diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration01Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration01Test.kt
index 580be7d705..6e1b663e3c 100644
--- a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration01Test.kt
+++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration01Test.kt
@@ -17,7 +17,7 @@ class AppMigration01Test {
val logsFileRemover = FakeLogFilesRemover()
val migration = AppMigration01(logsFileRemover)
- migration.migrate()
+ migration.migrate(true)
logsFileRemover.performLambda.assertions().isCalledOnce()
}
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 fb7dbf5281..08c10160b8 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
@@ -30,7 +30,7 @@ class AppMigration02Test {
)
val migration = AppMigration02(sessionStore = sessionStore, sessionPreferenceStoreFactory = sessionPreferencesStoreFactory)
- migration.migrate()
+ migration.migrate(true)
// We got the session preferences store
sessionPreferencesStoreFactory.getLambda.assertions().isCalledOnce()
diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration03Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration03Test.kt
index e4911faaa5..0f4db78aa2 100644
--- a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration03Test.kt
+++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration03Test.kt
@@ -17,7 +17,7 @@ class AppMigration03Test {
val logsFileRemover = FakeLogFilesRemover()
val migration = AppMigration03(migration01 = AppMigration01(logsFileRemover))
- migration.migrate()
+ migration.migrate(true)
logsFileRemover.performLambda.assertions().isCalledOnce()
}
diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04Test.kt
index f272dadc22..68cbfde689 100644
--- a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04Test.kt
+++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04Test.kt
@@ -28,7 +28,7 @@ class AppMigration04Test {
val migration = AppMigration04(context)
- migration.migrate()
+ migration.migrate(true)
// Check that the file has been deleted
assertThat(file.exists()).isFalse()
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 af71905635..ff5b8cd40a 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
@@ -27,7 +27,7 @@ class AppMigration05Test {
)
)
val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path"))
- migration.migrate()
+ migration.migrate(true)
val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
assertThat(storedData.sessionPath).isEqualTo("/a/path/${A_SESSION_ID.value.replace(':', '_')}")
}
@@ -43,7 +43,7 @@ class AppMigration05Test {
)
)
val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path"))
- migration.migrate()
+ migration.migrate(true)
val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
assertThat(storedData.sessionPath).isEqualTo("/a/path/existing")
}
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 095085cd17..e7e15ba821 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
@@ -28,7 +28,7 @@ class AppMigration06Test {
)
)
val migration = AppMigration06(sessionStore = sessionStore, cacheDirectory = File("/a/path/cache"))
- migration.migrate()
+ migration.migrate(true)
val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
assertThat(storedData.cachePath).isEqualTo("/a/path/cache/AN_ID")
}
@@ -44,7 +44,7 @@ class AppMigration06Test {
)
)
val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path/cache"))
- migration.migrate()
+ migration.migrate(true)
val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
assertThat(storedData.cachePath).isEqualTo("/a/path/existing")
}
diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration07Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration07Test.kt
index a375ef8563..a2575b32df 100644
--- a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration07Test.kt
+++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration07Test.kt
@@ -24,7 +24,7 @@ class AppMigration07Test {
}
val logsFileRemover = FakeLogFilesRemover(performLambda = performLambda)
val migration = AppMigration07(logsFileRemover)
- migration.migrate()
+ migration.migrate(true)
performLambda.assertions().isCalledOnce()
}
}
diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration08Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration08Test.kt
new file mode 100644
index 0000000000..75ffa28882
--- /dev/null
+++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration08Test.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.migration.impl.migrations
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
+import io.element.android.tests.testutils.lambda.lambdaError
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class AppMigration08Test {
+ @Test
+ fun `migration on fresh install should not invoke the AnnouncementService`() = runTest {
+ val service = FakeAnnouncementService(
+ showAnnouncementResult = { lambdaError() },
+ )
+ val migration = AppMigration08(service)
+ migration.migrate(isFreshInstall = true)
+ assertThat(service.announcementsToShowFlow().first()).isEmpty()
+ }
+
+ @Test
+ fun `migration on upgrade should invoke the AnnouncementService`() = runTest {
+ val showAnnouncementResult = lambdaRecorder { }
+ val service = FakeAnnouncementService(
+ showAnnouncementResult = showAnnouncementResult,
+ )
+ val migration = AppMigration08(service)
+ migration.migrate(isFreshInstall = false)
+ showAnnouncementResult.assertions().isCalledOnce()
+ .with(value(Announcement.NewNotificationSound))
+ }
+}
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 943ef5bd38..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
@@ -14,7 +14,7 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.features.poll.api.create.CreatePollMode
@@ -26,7 +26,7 @@ import io.element.android.services.analytics.api.AnalyticsService
import java.util.concurrent.atomic.AtomicBoolean
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class CreatePollNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 2d760d8314..5136571acd 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
@@ -18,7 +18,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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
@@ -33,11 +33,10 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
-import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.launch
import timber.log.Timber
-@Inject
+@AssistedInject
class CreatePollPresenter(
repositoryFactory: PollRepository.Factory,
private val analyticsService: AnalyticsService,
@@ -79,7 +78,7 @@ class CreatePollPresenter(
repository.getPoll(mode.eventId).onSuccess {
val loadedPoll = PollFormState(
question = it.question,
- answers = it.answers.map(PollAnswer::text).toPersistentList(),
+ answers = it.answers.map(PollAnswer::text).toImmutableList(),
isDisclosed = it.kind.isDisclosed,
)
initialPoll = loadedPoll
diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt
index dd773c97fb..f1f345b468 100644
--- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt
+++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt
@@ -9,8 +9,7 @@ package io.element.android.features.poll.impl.create
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.poll.PollKind
-import kotlinx.collections.immutable.PersistentList
-import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
class CreatePollStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -20,7 +19,7 @@ class CreatePollStateProvider : PreviewParameterProvider {
canCreate = false,
canAddAnswer = true,
question = "",
- answers = persistentListOf(
+ answers = listOf(
Answer("", false),
Answer("", false)
),
@@ -33,7 +32,7 @@ class CreatePollStateProvider : PreviewParameterProvider {
canCreate = true,
canAddAnswer = true,
question = "What type of food should we have?",
- answers = persistentListOf(
+ answers = listOf(
Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", false),
Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", false),
),
@@ -46,7 +45,7 @@ class CreatePollStateProvider : PreviewParameterProvider {
canCreate = true,
canAddAnswer = true,
question = "What type of food should we have?",
- answers = persistentListOf(
+ answers = listOf(
Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", false),
Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", false),
),
@@ -59,7 +58,7 @@ class CreatePollStateProvider : PreviewParameterProvider {
canCreate = true,
canAddAnswer = true,
question = "What type of food should we have?",
- answers = persistentListOf(
+ answers = listOf(
Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", true),
Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", true),
Answer("Brazilian \uD83C\uDDE7\uD83C\uDDF7", true),
@@ -74,7 +73,7 @@ class CreatePollStateProvider : PreviewParameterProvider {
canCreate = true,
canAddAnswer = false,
question = "Should there be more than 20 answers?",
- answers = persistentListOf(
+ answers = listOf(
Answer("1", true),
Answer("2", true),
Answer("3", true),
@@ -108,7 +107,7 @@ class CreatePollStateProvider : PreviewParameterProvider {
" Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor" +
" in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt" +
" in culpa qui officia deserunt mollit anim id est laborum.",
- answers = persistentListOf(
+ answers = listOf(
Answer(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." +
" Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis a.",
@@ -129,7 +128,7 @@ class CreatePollStateProvider : PreviewParameterProvider {
canCreate = false,
canAddAnswer = true,
question = "",
- answers = persistentListOf(
+ answers = listOf(
Answer("", false),
Answer("", false)
),
@@ -142,7 +141,7 @@ class CreatePollStateProvider : PreviewParameterProvider {
canCreate = false,
canAddAnswer = true,
question = "",
- answers = persistentListOf(
+ answers = listOf(
Answer("", false),
Answer("", false)
),
@@ -158,7 +157,7 @@ private fun aCreatePollState(
canCreate: Boolean,
canAddAnswer: Boolean,
question: String,
- answers: PersistentList,
+ answers: List,
showBackConfirmation: Boolean,
showDeleteConfirmation: Boolean,
pollKind: PollKind
@@ -168,7 +167,7 @@ private fun aCreatePollState(
canSave = canCreate,
canAddAnswer = canAddAnswer,
question = question,
- answers = answers,
+ answers = answers.toImmutableList(),
showBackConfirmation = showBackConfirmation,
showDeleteConfirmation = showDeleteConfirmation,
pollKind = pollKind,
diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/PollFormState.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/PollFormState.kt
index 6ac7797205..34b9d6206b 100644
--- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/PollFormState.kt
+++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/PollFormState.kt
@@ -13,7 +13,7 @@ import io.element.android.features.poll.impl.PollConstants
import io.element.android.features.poll.impl.PollConstants.MIN_ANSWERS
import io.element.android.libraries.matrix.api.poll.PollKind
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
/**
* Represents the state of the poll creation / edit form.
@@ -28,7 +28,7 @@ data class PollFormState(
companion object {
val Empty = PollFormState(
question = "",
- answers = MutableList(MIN_ANSWERS) { "" }.toPersistentList(),
+ answers = MutableList(MIN_ANSWERS) { "" }.toImmutableList(),
isDisclosed = true,
)
}
@@ -49,7 +49,7 @@ data class PollFormState(
return this
}
- return copy(answers = (answers + "").toPersistentList())
+ return copy(answers = (answers + "").toImmutableList())
}
/**
@@ -66,7 +66,7 @@ data class PollFormState(
return this
}
- return copy(answers = answers.filterIndexed { i, _ -> i != index }.toPersistentList())
+ return copy(answers = answers.filterIndexed { i, _ -> i != index }.toImmutableList())
}
/**
@@ -82,7 +82,7 @@ data class PollFormState(
fun withAnswerChanged(index: Int, rawAnswer: String): PollFormState =
copy(answers = answers.toMutableList().apply {
this[index] = rawAnswer.take(PollConstants.MAX_ANSWER_LENGTH)
- }.toPersistentList())
+ }.toImmutableList())
/**
* Whether a new answer can be added.
@@ -114,7 +114,7 @@ internal val pollFormStateSaver = mapSaver(
restore = { saved ->
PollFormState(
question = saved["question"] as String,
- answers = (saved["answers"] as Array<*>).map { it as String }.toPersistentList(),
+ answers = (saved["answers"] as Array<*>).map { it as String }.toImmutableList(),
isDisclosed = saved["isDisclosed"] as Boolean,
)
}
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 983e3157a3..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
@@ -9,7 +9,7 @@ package io.element.android.features.poll.impl.data
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
-@Inject
+@AssistedInject
class PollRepository(
private val room: JoinedRoom,
private val defaultTimelineProvider: TimelineProvider,
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 f1710712c0..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
@@ -16,7 +16,7 @@ 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.Inject
+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
@@ -29,7 +29,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class PollHistoryFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 2b6f79fcf4..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
@@ -14,13 +14,13 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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)
-@Inject
+@AssistedInject
class PollHistoryNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryStateProvider.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryStateProvider.kt
index e8161d9fe1..2d126ce7cf 100644
--- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryStateProvider.kt
+++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryStateProvider.kt
@@ -13,7 +13,7 @@ import io.element.android.features.poll.api.pollcontent.aPollContentState
import io.element.android.features.poll.impl.history.model.PollHistoryFilter
import io.element.android.features.poll.impl.history.model.PollHistoryItem
import io.element.android.features.poll.impl.history.model.PollHistoryItems
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
class PollHistoryStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -53,8 +53,8 @@ internal fun aPollHistoryState(
hasMoreToLoad = hasMoreToLoad,
activeFilter = activeFilter,
pollHistoryItems = PollHistoryItems(
- ongoing = currentItems.toPersistentList(),
- past = currentItems.toPersistentList(),
+ ongoing = currentItems.toImmutableList(),
+ past = currentItems.toImmutableList(),
),
eventSink = eventSink,
)
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 2a9c802e6b..c895a2c16c 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
@@ -14,7 +14,7 @@ import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.withContext
@Inject
@@ -36,8 +36,8 @@ class PollHistoryItemsFactory(
}
}
PollHistoryItems(
- ongoing = ongoing.toPersistentList(),
- past = past.toPersistentList()
+ ongoing = ongoing.toImmutableList(),
+ past = past.toImmutableList()
)
}
diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateSaverTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateSaverTest.kt
index b400bef2e6..bab6171477 100644
--- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateSaverTest.kt
+++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateSaverTest.kt
@@ -9,7 +9,7 @@ package io.element.android.features.poll.impl.create
import androidx.compose.runtime.saveable.SaverScope
import com.google.common.truth.Truth.assertThat
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.persistentListOf
import org.junit.Test
class PollFormStateSaverTest {
@@ -21,7 +21,7 @@ class PollFormStateSaverTest {
fun `test save and restore`() {
val state = PollFormState(
question = "question",
- answers = listOf("answer1", "answer2").toPersistentList(),
+ answers = persistentListOf("answer1", "answer2"),
isDisclosed = true,
)
diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateTest.kt
index f158e9c803..9594903c69 100644
--- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateTest.kt
+++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateTest.kt
@@ -10,7 +10,7 @@ package io.element.android.features.poll.impl.create
import com.google.common.truth.Truth.assertThat
import io.element.android.features.poll.impl.PollConstants
import io.element.android.libraries.matrix.api.poll.PollKind
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import org.junit.Test
class PollFormStateTest {
@@ -47,7 +47,7 @@ class PollFormStateTest {
val state = PollFormState.Empty
val newState = state.withAnswerChanged(1, "New answer")
assertThat(newState).isEqualTo(PollFormState.Empty.copy(
- answers = listOf("", "New answer").toPersistentList()
+ answers = listOf("", "New answer").toImmutableList()
))
}
@@ -58,7 +58,7 @@ class PollFormStateTest {
val state = PollFormState.Empty
val newState = state.withAnswerChanged(1, tooLongAnswer)
assertThat(newState).isEqualTo(PollFormState.Empty.copy(
- answers = listOf("", truncatedAnswer).toPersistentList()
+ answers = listOf("", truncatedAnswer).toImmutableList()
))
}
@@ -101,7 +101,7 @@ class PollFormStateTest {
@Test
fun `is valid is false when not enough answers`() {
- val state = aValidPollFormState().copy(answers = listOf("").toPersistentList())
+ val state = aValidPollFormState().copy(answers = listOf("").toImmutableList())
assertThat(state.isValid).isFalse()
}
@@ -127,10 +127,10 @@ class PollFormStateTest {
private fun aValidPollFormState(): PollFormState {
return PollFormState.Empty.copy(
question = "question",
- answers = listOf("answer1", "answer2").toPersistentList(),
+ answers = listOf("answer1", "answer2").toImmutableList(),
isDisclosed = true,
)
}
private fun PollFormState.withBlankAnswers(numAnswers: Int): PollFormState =
- copy(answers = List(numAnswers) { "" }.toPersistentList())
+ copy(answers = List(numAnswers) { "" }.toImmutableList())
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 d3edba5833..eb057a9d53 100644
--- a/features/preferences/impl/build.gradle.kts
+++ b/features/preferences/impl/build.gradle.kts
@@ -105,6 +105,7 @@ dependencies {
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)
}
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 49e297af08..e4ba87c43a 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
@@ -18,7 +18,7 @@ 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.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -30,6 +30,7 @@ import io.element.android.features.preferences.impl.advanced.AdvancedSettingsNod
import io.element.android.features.preferences.impl.analytics.AnalyticsSettingsNode
import io.element.android.features.preferences.impl.blockedusers.BlockedUsersNode
import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode
+import io.element.android.features.preferences.impl.labs.LabsNode
import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingNode
import io.element.android.features.preferences.impl.root.PreferencesRootNode
@@ -41,14 +42,13 @@ 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)
-@Inject
+@AssistedInject
class PreferencesFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
@@ -76,6 +76,9 @@ class PreferencesFlowNode(
@Parcelize
data object AdvancedSettings : NavTarget
+ @Parcelize
+ data object Labs : NavTarget
+
@Parcelize
data object AnalyticsSettings : NavTarget
@@ -117,6 +120,10 @@ class PreferencesFlowNode(
return when (navTarget) {
NavTarget.Root -> {
val callback = object : PreferencesRootNode.Callback {
+ override fun onAddAccount() {
+ plugins().forEach { it.onAddAccount() }
+ }
+
override fun onOpenBugReport() {
plugins().forEach { it.onOpenBugReport() }
}
@@ -149,6 +156,10 @@ class PreferencesFlowNode(
backstack.push(NavTarget.AdvancedSettings)
}
+ override fun onOpenLabs() {
+ backstack.push(NavTarget.Labs)
+ }
+
override fun onOpenUserProfile(matrixUser: MatrixUser) {
backstack.push(NavTarget.UserProfile(matrixUser))
}
@@ -175,6 +186,9 @@ class PreferencesFlowNode(
}
createNode(buildContext, listOf(developerSettingsCallback))
}
+ NavTarget.Labs -> {
+ createNode(buildContext)
+ }
NavTarget.About -> {
val callback = object : AboutNode.Callback {
override fun openOssLicenses() {
@@ -226,8 +240,8 @@ class PreferencesFlowNode(
}
}
- 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()
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 bedd778ee7..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
@@ -15,14 +15,14 @@ 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.Inject
+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)
-@Inject
+@AssistedInject
class AboutNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 d4a5cbea37..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
@@ -13,12 +13,12 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class AdvancedSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt
index 6f34d216f6..e1c6ce259a 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt
@@ -45,7 +45,7 @@ import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
@Composable
fun AdvancedSettingsView(
@@ -73,7 +73,7 @@ fun AdvancedSettingsView(
PreferenceDropdown(
title = stringResource(id = CommonStrings.common_appearance),
selectedOption = state.theme,
- options = ThemeOption.entries.toPersistentList(),
+ options = ThemeOption.entries.toImmutableList(),
onSelectOption = { themeOption ->
state.eventSink(AdvancedSettingsEvents.SetTheme(themeOption))
}
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 07b32b8d07..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
@@ -13,12 +13,12 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class AnalyticsSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 89a4a6c571..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
@@ -13,12 +13,12 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class BlockedUsersNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 8a61bd4bed..5fbbe3e797 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
@@ -25,7 +25,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -83,7 +83,7 @@ class BlockedUsersPresenter(
}
}
return BlockedUsersState(
- blockedUsers = ignoredMatrixUser.toPersistentList(),
+ blockedUsers = ignoredMatrixUser.toImmutableList(),
unblockUserAction = unblockUserAction.value,
eventSink = ::handleEvents
)
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt
index ba90f93ba5..ac344ba838 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt
@@ -11,7 +11,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
class BlockedUsersStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -32,7 +32,7 @@ internal fun aBlockedUsersState(
eventSink: (BlockedUsersEvents) -> Unit = {},
): BlockedUsersState {
return BlockedUsersState(
- blockedUsers = blockedUsers.toPersistentList(),
+ blockedUsers = blockedUsers.toImmutableList(),
unblockUserAction = unblockUserAction,
eventSink = eventSink,
)
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 b1761e54c0..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
@@ -16,13 +16,13 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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)
-@Inject
+@AssistedInject
class DeveloperSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 e07c30bac9..fe7a3461b8 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
@@ -37,11 +37,9 @@ import io.element.android.libraries.featureflag.api.Feature
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
-import io.element.android.libraries.matrix.api.tracing.TraceLogPack
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
-import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
@@ -82,16 +80,16 @@ class DeveloperSettingsPresenter(
appPreferencesStore.getTracingLogLevelFlow().map { AsyncData.Success(it.toLogLevelItem()) }
}
val tracingLogLevel by tracingLogLevelFlow.collectAsState(initial = AsyncData.Uninitialized)
- val tracingLogPacks by produceState(persistentListOf()) {
+ val tracingLogPacks by produceState(persistentListOf()) {
appPreferencesStore.getTracingLogPacksFlow()
// Sort the entries alphabetically by its title
- .map { it.sortedBy { it.title }.toPersistentList() }
- .collectLatest { value = it }
+ .map { it.sortedBy { it.title } }
+ .collectLatest { value = it.toImmutableList() }
}
LaunchedEffect(Unit) {
- FeatureFlags.entries
- .filter { it.isFinished.not() }
+ featureFlagService.getAvailableFeatures()
+ .filter { it.isInLabs.not() && it.isFinished.not() }
.run {
// Never display room directory search in release builds for Play Store
if (buildMeta.flavorDescription == "GooglePlay" && buildMeta.buildType == BuildType.RELEASE) {
@@ -169,6 +167,7 @@ class DeveloperSettingsPresenter(
key = feature.key,
title = feature.title,
description = feature.description,
+ icon = null,
isEnabled = isEnabled
)
}
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt
index 1585a3b8dd..6ccd857552 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt
@@ -14,7 +14,7 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
open class DeveloperSettingsStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -43,7 +43,7 @@ fun aDeveloperSettingsState(
clearCacheAction = clearCacheAction,
customElementCallBaseUrlState = customElementCallBaseUrlState,
tracingLogLevel = AsyncData.Success(LogLevelItem.INFO),
- tracingLogPacks = traceLogPacks.toPersistentList(),
+ tracingLogPacks = traceLogPacks.toImmutableList(),
eventSink = eventSink,
)
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt
index e00b178dee..97f08f7a16 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt
@@ -36,7 +36,7 @@ import io.element.android.libraries.featureflag.ui.FeatureListView
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
import io.element.android.libraries.ui.strings.CommonStrings
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
@Composable
fun DeveloperSettingsView(
@@ -66,7 +66,7 @@ fun DeveloperSettingsView(
title = "Tracing log level",
supportingText = "Requires app reboot",
selectedOption = state.tracingLogLevel.dataOrNull(),
- options = LogLevelItem.entries.toPersistentList(),
+ options = LogLevelItem.entries.toImmutableList(),
onSelectOption = { logLevel ->
state.eventSink(DeveloperSettingsEvents.SetTracingLogLevel(logLevel))
}
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsEvents.kt
new file mode 100644
index 0000000000..0f652a5c5c
--- /dev/null
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsEvents.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.preferences.impl.labs
+
+import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
+
+sealed interface LabsEvents {
+ data class ToggleFeature(val feature: FeatureUiModel) : LabsEvents
+}
diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsNode.kt
similarity index 53%
rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceNode.kt
rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsNode.kt
index a4828fe9fe..5c2b5ed0e3 100644
--- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceNode.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsNode.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.space.impl
+package io.element.android.features.preferences.impl.labs
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -13,33 +13,20 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
-import io.element.android.features.space.api.SpaceEntryPoint
-import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
-class SpaceNode(
+@AssistedInject
+class LabsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- presenterFactory: SpacePresenter.Factory,
+ private val presenter: LabsPresenter,
) : Node(buildContext, plugins = plugins) {
- private val inputs: SpaceEntryPoint.Inputs = inputs()
- private val callback = plugins.filterIsInstance().single()
- private val presenter = presenterFactory.create(inputs)
-
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
- SpaceView(
- state = state,
- onBackClick = ::navigateUp,
- onRoomClick = { roomId ->
- callback.onOpenRoom(roomId)
- },
- modifier = modifier
- )
+ LabsView(state = state, onBack = ::navigateUp)
}
}
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsPresenter.kt
new file mode 100644
index 0000000000..5e74454d75
--- /dev/null
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsPresenter.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.preferences.impl.labs
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateMapOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateMap
+import dev.zacsweers.metro.Inject
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.features.preferences.impl.R
+import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.core.bool.orFalse
+import io.element.android.libraries.designsystem.theme.components.IconSource
+import io.element.android.libraries.featureflag.api.Feature
+import io.element.android.libraries.featureflag.api.FeatureFlagService
+import io.element.android.libraries.featureflag.api.FeatureFlags
+import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
+import io.element.android.services.toolbox.api.strings.StringProvider
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.launch
+
+@Inject
+class LabsPresenter(
+ private val stringProvider: StringProvider,
+ private val featureFlagService: FeatureFlagService,
+ private val clearCacheUseCase: ClearCacheUseCase,
+) : Presenter {
+ @Composable
+ override fun present(): LabsState {
+ val coroutineScope = rememberCoroutineScope()
+ val features = remember {
+ val entries = featureFlagService.getAvailableFeatures()
+ .filter { it.isInLabs && !it.isFinished }
+ .map { it.key to it }
+ mutableStateMapOf(*entries.toTypedArray())
+ }
+ val enabledFeatures = remember {
+ mutableStateMapOf()
+ }
+
+ LaunchedEffect(Unit) {
+ for (feature in features.values) {
+ val isEnabled = featureFlagService.isFeatureEnabled(feature)
+ enabledFeatures[feature.key] = isEnabled
+ }
+ }
+
+ var isApplyingChanges by remember { mutableStateOf(false) }
+
+ val featureUiModels = createUiModels(features, enabledFeatures)
+
+ fun handleEvent(event: LabsEvents) {
+ when (event) {
+ is LabsEvents.ToggleFeature -> coroutineScope.launch {
+ val feature = features[event.feature.key] ?: return@launch
+ val isEnabled = featureFlagService.isFeatureEnabled(feature)
+ featureFlagService.setFeatureEnabled(feature = feature, enabled = !isEnabled)
+ enabledFeatures[feature.key] = !isEnabled
+
+ when (feature.key) {
+ FeatureFlags.Threads.key -> {
+ // Threads require a cache clear to recreate the event cache
+ clearCacheUseCase()
+ isApplyingChanges = true
+ }
+ }
+ }
+ }
+ }
+
+ return LabsState(
+ features = featureUiModels,
+ isApplyingChanges = isApplyingChanges,
+ eventSink = ::handleEvent,
+ )
+ }
+
+ @Composable
+ private fun createUiModels(
+ features: SnapshotStateMap,
+ enabledFeatures: SnapshotStateMap
+ ): ImmutableList {
+ return features.values.map { feature ->
+ key(feature.key) {
+ val isEnabled = enabledFeatures[feature.key].orFalse()
+ val title = when (feature) {
+ FeatureFlags.Threads -> stringProvider.getString(R.string.screen_labs_enable_threads)
+ else -> feature.title
+ }
+ val description = when (feature) {
+ FeatureFlags.Threads -> stringProvider.getString(R.string.screen_labs_enable_threads_description)
+ else -> feature.description
+ }
+ val icon = when (feature) {
+ FeatureFlags.Threads -> CompoundIcons.Threads()
+ else -> null
+ }
+ remember(feature, isEnabled) {
+ FeatureUiModel(
+ key = feature.key,
+ title = title,
+ description = description,
+ icon = icon?.let(IconSource::Vector),
+ isEnabled = isEnabled
+ )
+ }
+ }
+ }.toImmutableList()
+ }
+}
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsState.kt
new file mode 100644
index 0000000000..0925cd7893
--- /dev/null
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsState.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.preferences.impl.labs
+
+import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
+import kotlinx.collections.immutable.ImmutableList
+
+data class LabsState(
+ val features: ImmutableList,
+ val isApplyingChanges: Boolean,
+ val eventSink: (LabsEvents) -> Unit,
+)
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsStateProvider.kt
new file mode 100644
index 0000000000..df804cd409
--- /dev/null
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsStateProvider.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.preferences.impl.labs
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.designsystem.icons.CompoundDrawables
+import io.element.android.libraries.designsystem.theme.components.IconSource
+import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
+import kotlinx.collections.immutable.toImmutableList
+
+internal class LabsStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aLabsState(features = aFeatureList()),
+ aLabsState(features = aFeatureList(), isApplyingChanges = true),
+ )
+}
+
+internal fun aLabsState(
+ features: List = emptyList(),
+ isApplyingChanges: Boolean = false,
+) = LabsState(
+ features = features.toImmutableList(),
+ isApplyingChanges = isApplyingChanges,
+ eventSink = {},
+)
+
+internal fun aFeatureList() = listOf(
+ FeatureUiModel(
+ key = "feature_1",
+ title = "Feature 1",
+ description = "This is a description of feature 1.",
+ isEnabled = true,
+ icon = IconSource.Resource(CompoundDrawables.ic_compound_threads),
+ ),
+ FeatureUiModel(
+ key = "feature_2",
+ title = "Feature 2",
+ description = "This is a description of feature 2.",
+ isEnabled = false,
+ icon = IconSource.Resource(CompoundDrawables.ic_compound_video_call),
+ )
+)
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsView.kt
new file mode 100644
index 0000000000..2738da4b63
--- /dev/null
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsView.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.preferences.impl.labs
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsPadding
+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.compound.tokens.generated.CompoundIcons
+import io.element.android.features.preferences.impl.R
+import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
+import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
+import io.element.android.libraries.designsystem.components.BigIcon
+import io.element.android.libraries.designsystem.components.ProgressDialog
+import io.element.android.libraries.designsystem.components.button.BackButton
+import io.element.android.libraries.designsystem.components.list.ListItemContent
+import io.element.android.libraries.designsystem.components.list.SwitchListItem
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.TopAppBar
+
+/**
+ * The contents of the Labs screen.
+ * Design: https://www.figma.com/design/V0dkfRAW6T3yCQKjahpzkX/ER-46-EX--Threads?node-id=2004-27319&t=yssy1yYYigsGON3s-0
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun LabsView(
+ state: LabsState,
+ onBack: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ if (state.isApplyingChanges) {
+ ProgressDialog()
+ }
+
+ BackHandler(
+ enabled = !state.isApplyingChanges,
+ onBack = onBack,
+ )
+
+ HeaderFooterPage(
+ modifier = modifier
+ .fillMaxSize()
+ .systemBarsPadding()
+ .imePadding(),
+ topBar = {
+ TopAppBar(
+ titleStr = stringResource(R.string.screen_labs_title),
+ navigationIcon = {
+ BackButton(onClick = onBack, enabled = !state.isApplyingChanges)
+ }
+ )
+ },
+ header = {
+ IconTitleSubtitleMolecule(
+ modifier = Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp),
+ title = stringResource(R.string.screen_labs_header_title),
+ subTitle = stringResource(R.string.screen_labs_header_description),
+ iconStyle = BigIcon.Style.Default(CompoundIcons.Labs())
+ )
+ },
+ contentPadding = PaddingValues(),
+ content = {
+ LazyColumn(
+ modifier = Modifier.fillMaxWidth(),
+ contentPadding = PaddingValues(horizontal = 10.dp, vertical = 20.dp),
+ ) {
+ items(items = state.features, key = { it.key }) { feature ->
+ SwitchListItem(
+ leadingContent = feature.icon?.let { ListItemContent.Icon(it) },
+ headline = feature.title,
+ supportingText = feature.description,
+ value = feature.isEnabled,
+ onChange = {
+ state.eventSink(LabsEvents.ToggleFeature(feature))
+ }
+ )
+ }
+ }
+ }
+ )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun LabsViewPreview(@PreviewParameter(LabsStateProvider::class) state: LabsState) {
+ ElementPreview {
+ LabsView(state = state, onBack = {})
+ }
+}
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 4303d6394e..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
@@ -14,12 +14,12 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class NotificationSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 3dd86ac224..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
@@ -14,7 +14,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -22,7 +22,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class EditDefaultNotificationSettingNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 fec0a5fdcc..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
@@ -17,7 +17,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import kotlinx.coroutines.launch
import java.text.Collator
import kotlin.time.Duration.Companion.seconds
-@Inject
+@AssistedInject
class EditDefaultNotificationSettingPresenter(
private val notificationSettingsService: NotificationSettingsService,
@Assisted private val isOneToOne: Boolean,
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 76febd58af..67d50a76f0 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
@@ -16,7 +16,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -26,7 +26,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.user.MatrixUser
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class PreferencesRootNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
@@ -34,6 +34,7 @@ class PreferencesRootNode(
private val directLogoutView: DirectLogoutView,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
+ fun onAddAccount()
fun onOpenBugReport()
fun onSecureBackupClick()
fun onOpenAnalytics()
@@ -42,12 +43,17 @@ class PreferencesRootNode(
fun onOpenNotificationSettings()
fun onOpenLockScreenSettings()
fun onOpenAdvancedSettings()
+ fun onOpenLabs()
fun onOpenUserProfile(matrixUser: MatrixUser)
fun onOpenBlockedUsers()
fun onSignOutClick()
fun onOpenAccountDeactivation()
}
+ private fun onAddAccount() {
+ plugins().forEach { it.onAddAccount() }
+ }
+
private fun onOpenBugReport() {
plugins().forEach { it.onOpenBugReport() }
}
@@ -64,6 +70,10 @@ class PreferencesRootNode(
plugins().forEach { it.onOpenAdvancedSettings() }
}
+ private fun onOpenLabs() {
+ plugins().forEach { it.onOpenLabs() }
+ }
+
private fun onOpenAnalytics() {
plugins().forEach { it.onOpenAnalytics() }
}
@@ -119,12 +129,14 @@ class PreferencesRootNode(
state = state,
modifier = modifier,
onBackClick = this::navigateUp,
+ onAddAccountClick = this::onAddAccount,
onOpenRageShake = this::onOpenBugReport,
onOpenAnalytics = this::onOpenAnalytics,
onOpenAbout = this::onOpenAbout,
onSecureBackupClick = this::onSecureBackupClick,
onOpenDeveloperSettings = this::onOpenDeveloperSettings,
onOpenAdvancedSettings = this::onOpenAdvancedSettings,
+ onOpenLabs = this::onOpenLabs,
onManageAccountClick = { onManageAccountClick(activity, it, isDark) },
onOpenNotificationSettings = this::onOpenNotificationSettings,
onOpenLockScreenSettings = this::onOpenLockScreenSettings,
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 aad8086df6..3da72f982e 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
@@ -24,13 +24,21 @@ 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.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@@ -45,6 +53,8 @@ class PreferencesRootPresenter(
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 {
@@ -55,6 +65,25 @@ class PreferencesRootPresenter(
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,
+ )
+ }
+ .toImmutableList()
+ }
+ }.collectAsState(initial = persistentListOf())
+
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
val hasAnalyticsProviders = remember { analyticsService.getAvailableAnalyticsProviders().isNotEmpty() }
@@ -83,6 +112,8 @@ class PreferencesRootPresenter(
.launchIn(this)
}
+ val showLabsItem = remember { featureFlagService.getAvailableFeatures().any { it.isInLabs && !it.isFinished } }
+
val directLogoutState = directLogoutPresenter.present()
LaunchedEffect(Unit) {
@@ -96,6 +127,9 @@ class PreferencesRootPresenter(
is PreferencesRootEvents.OnVersionInfoClick -> {
showDeveloperSettingsProvider.unlockDeveloperSettings(coroutineScope)
}
+ is PreferencesRootEvents.SwitchToSession -> coroutineScope.launch {
+ sessionStore.setLatestSession(event.sessionId.value)
+ }
}
}
@@ -103,6 +137,8 @@ class PreferencesRootPresenter(
myUser = matrixUser.value,
version = versionFormatter.get(),
deviceId = matrixClient.deviceId,
+ isMultiAccountEnabled = isMultiAccountEnabled,
+ otherSessions = otherSessions,
showSecureBackup = !canVerifyUserSession,
showSecureBackupBadge = showSecureBackupIndicator,
accountManagementUrl = accountManagementUrl.value,
@@ -112,6 +148,7 @@ class PreferencesRootPresenter(
showDeveloperSettings = showDeveloperSettings,
canDeactivateAccount = canDeactivateAccount,
showBlockedUsersItem = showBlockedUsersItem,
+ showLabsItem = showLabsItem,
directLogoutState = directLogoutState,
snackbarMessage = snackbarMessage,
eventSink = ::handleEvent,
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..3df13e0efd 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?,
@@ -25,6 +28,7 @@ data class PreferencesRootState(
val showDeveloperSettings: Boolean,
val canDeactivateAccount: Boolean,
val showBlockedUsersItem: Boolean,
+ val showLabsItem: Boolean,
val directLogoutState: DirectLogoutState,
val snackbarMessage: SnackbarMessage?,
val eventSink: (PreferencesRootEvents) -> Unit,
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt
index 91b32fe12d..ab30d07bcf 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.toImmutableList
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.toImmutableList(),
showSecureBackup = true,
showSecureBackupBadge = true,
accountManagementUrl = "aUrl",
@@ -28,6 +33,7 @@ fun aPreferencesRootState(
canReportBug = true,
showDeveloperSettings = true,
showBlockedUsersItem = true,
+ showLabsItem = true,
canDeactivateAccount = true,
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
directLogoutState = aDirectLogoutState(),
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 544b5f5b3e..3b4d2d2670 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,
@@ -52,6 +59,7 @@ fun PreferencesRootView(
onOpenAbout: () -> Unit,
onOpenDeveloperSettings: () -> Unit,
onOpenAdvancedSettings: () -> Unit,
+ onOpenLabs: () -> Unit,
onOpenNotificationSettings: () -> Unit,
onOpenUserProfile: (MatrixUser) -> Unit,
onOpenBlockedUsers: () -> Unit,
@@ -74,7 +82,12 @@ fun PreferencesRootView(
},
user = state.myUser,
)
-
+ if (state.isMultiAccountEnabled) {
+ MultiAccountSection(
+ state = state,
+ onAddAccountClick = onAddAccountClick,
+ )
+ }
// 'Manage my app' section
ManageAppSection(
state = state,
@@ -98,6 +111,7 @@ fun PreferencesRootView(
onOpenRageShake = onOpenRageShake,
onOpenAdvancedSettings = onOpenAdvancedSettings,
onOpenDeveloperSettings = onOpenDeveloperSettings,
+ onOpenLabs = onOpenLabs,
onSignOutClick = onSignOutClick,
onDeactivateClick = onDeactivateClick,
)
@@ -114,6 +128,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,
@@ -186,6 +232,7 @@ private fun ColumnScope.GeneralSection(
onOpenAnalytics: () -> Unit,
onOpenRageShake: () -> Unit,
onOpenAdvancedSettings: () -> Unit,
+ onOpenLabs: () -> Unit,
onOpenDeveloperSettings: () -> Unit,
onSignOutClick: () -> Unit,
onDeactivateClick: () -> Unit,
@@ -214,6 +261,15 @@ private fun ColumnScope.GeneralSection(
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Settings())),
onClick = onOpenAdvancedSettings,
)
+
+ if (state.showLabsItem) {
+ ListItem(
+ headlineContent = { Text(stringResource(id = R.string.screen_labs_title)) },
+ leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Labs())),
+ onClick = onOpenLabs,
+ )
+ }
+
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.action_signout)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.SignOut())),
@@ -287,10 +343,12 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
PreferencesRootView(
state = aPreferencesRootState(myUser = matrixUser),
onBackClick = {},
+ onAddAccountClick = {},
onOpenAnalytics = {},
onOpenRageShake = {},
onOpenDeveloperSettings = {},
onOpenAdvancedSettings = {},
+ onOpenLabs = {},
onOpenAbout = {},
onSecureBackupClick = {},
onManageAccountClick = {},
@@ -302,3 +360,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/user/editprofile/EditUserProfileNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt
index 64d3e73447..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
@@ -13,7 +13,7 @@ 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.Inject
+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
@@ -21,7 +21,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.user.MatrixUser
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class EditUserProfileNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 f6069bc442..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
@@ -21,7 +21,7 @@ import androidx.compose.runtime.setValue
import androidx.core.net.toUri
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
-@Inject
+@AssistedInject
class EditUserProfilePresenter(
@Assisted private val matrixUser: MatrixUser,
private val matrixClient: MatrixClient,
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-da/translations.xml b/features/preferences/impl/src/main/res/values-da/translations.xml
index acf9fe4087..f258fb0952 100644
--- a/features/preferences/impl/src/main/res/values-da/translations.xml
+++ b/features/preferences/impl/src/main/res/values-da/translations.xml
@@ -10,6 +10,7 @@
"Ugyldig URL, sørg for at inkludere protokollen (http/https) og den korrekte adresse."
"Skjul avatarer i anmodninger om invitation til rum"
"Skjul forhåndsvisning af medier i tidslinjen"
+ "Laboratorier"
"Upload fotos og videoer hurtigere, og reducér dataforbrug"
"Optimér mediekvaliteten"
"Moderation og sikkerhed"
@@ -43,6 +44,11 @@
"Kan ikke opdatere profilen"
"Redigér profil"
"Opdaterer profil…"
+ "Aktivér svar-tråde"
+ "Appen genstarter for at anvende denne ændring."
+ "Prøv vores nyeste idéer under udvikling. Disse funktioner er ikke færdige; de kan være ustabile og kan ændre sig."
+ "Er du i humør til at eksperimentere?"
+ "Laboratorier"
"Yderligere indstillinger"
"Lyd- og videoopkald"
"Uoverensstemmelse i konfigurationen"
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 4072207fb4..c3722ec88c 100644
--- a/features/preferences/impl/src/main/res/values-de/translations.xml
+++ b/features/preferences/impl/src/main/res/values-de/translations.xml
@@ -10,6 +10,7 @@
"Ungültige URL, bitte gib das Protokoll (http/https) und die richtige Adresse an."
"Avatare in Chateinladungen ausblenden"
"Medienvorschau im Nachrichtenverlauf ausblenden"
+ "Labs"
"Lade Fotos und Videos schneller hoch und reduziere den Datenverbrauch"
"Optimiere die Medienqualität"
"Moderation und Sicherheit"
@@ -43,6 +44,11 @@
"Profil kann nicht aktualisiert werden"
"Profil bearbeiten"
"Profil wird aktualisiert…"
+ "Thread-Antworten aktivieren"
+ "Die App wird neu gestartet, um diese Änderung zu übernehmen."
+ "Probier unsere neuesten Ideen in der Entwicklung aus. Diese Funktionen sind noch nicht fertiggestellt; sie können instabil sein und sich noch ändern."
+ "Entdeckungsfreudig?"
+ "Labs"
"Zusätzliche Einstellungen"
"Audio- und Videoanrufe"
"Konfiguration stimmt nicht überein"
diff --git a/features/preferences/impl/src/main/res/values-fa/translations.xml b/features/preferences/impl/src/main/res/values-fa/translations.xml
index 1ca2ad38db..22ff843dc2 100644
--- a/features/preferences/impl/src/main/res/values-fa/translations.xml
+++ b/features/preferences/impl/src/main/res/values-fa/translations.xml
@@ -58,7 +58,7 @@
"تنظیمات سامانه"
"آگاهیهای سامانهای خاموش شدند"
"آگاهیها"
- "تاریخچهٔ فرستادن"
+ "تاریخچهٔ آگاهیهای ارسالی"
"رفعاشکال"
"رفعاشکال آگاهیها"
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 1240b620e9..f1462f3bb7 100644
--- a/features/preferences/impl/src/main/res/values-fi/translations.xml
+++ b/features/preferences/impl/src/main/res/values-fi/translations.xml
@@ -10,6 +10,7 @@
"Virheellinen URL-osoite. Varmista, että sisällytät protokollan (http/https) ja oikean osoitteen."
"Piilota huoneiden avatarit kutsuista"
"Piilota median esikatselu aikajanalla"
+ "Labrat"
"Lähetä valokuvia ja videoita nopeammin ja vähennä datan käyttöä."
"Optimoi median laatu"
"Moderointi ja Turvallisuus"
@@ -43,6 +44,11 @@
"Profiilin muokkaaminen ei onnistunut"
"Muokkaa profiilia"
"Muokataan profiilia…"
+ "Ota käyttöön viestiketjuvastaukset"
+ "Sovellus käynnistyy uudelleen muutoksen käyttöönottamiseksi."
+ "Kokeile uusimpia kehitteillä olevia ideoitamme. Nämä ominaisuudet eivät ole vielä valmiita; ne voivat olla epävakaita ja muuttua."
+ "Kokeilunhaluinen olo?"
+ "Labrat"
"Lisäasetukset"
"Ääni- ja videopuheluista"
"Konfiguraatio ei täsmää"
diff --git a/features/preferences/impl/src/main/res/values-fr/translations.xml b/features/preferences/impl/src/main/res/values-fr/translations.xml
index dbb0b13326..5ee91388bd 100644
--- a/features/preferences/impl/src/main/res/values-fr/translations.xml
+++ b/features/preferences/impl/src/main/res/values-fr/translations.xml
@@ -10,6 +10,7 @@
"URL invalide, assurez-vous d’inclure le protocol (http/https) et l’adresse correcte."
"Masquer les avatars des salons dans les invitations"
"Masquer les aperçus des médias dans les discussions"
+ "Expérimental"
"Téléchargez des photos et des vidéos plus rapidement et réduisez la consommation de données"
"Optimisez la qualité des médias"
"Modération et sécurité"
@@ -43,6 +44,11 @@
"Impossible de mettre à jour le profil"
"Modifier le profil"
"Mise à jour du profil…"
+ "Activez les fils de discussion."
+ "Un changement entraînera le redémarrage de l’application."
+ "Découvrez nos dernières idées en cours de développement. Ces fonctionnalités ne sont pas encore finalisées; elles peuvent être instables et évoluer."
+ "Envie d’expérimenter?"
+ "Expérimental"
"Réglages supplémentaires"
"Appels audio et vidéo"
"Incompatibilité de configuration"
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..9cc5a97a44 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ények"
"Hibaelhárítás"
"Értesítések hibaelhárítása"
diff --git a/features/preferences/impl/src/main/res/values-nb/translations.xml b/features/preferences/impl/src/main/res/values-nb/translations.xml
index 84283b898b..90ec12a1a1 100644
--- a/features/preferences/impl/src/main/res/values-nb/translations.xml
+++ b/features/preferences/impl/src/main/res/values-nb/translations.xml
@@ -10,9 +10,17 @@
"Ugyldig URL. Pass på at du inkluderer protokollen (http/https) og riktig adresse."
"Skjul avatarer i invitasjonsforespørsler til rom"
"Skjul forhåndsvisninger av medier på tidslinjen"
+ "Prøvefunksjoner"
"Last opp bilder og videoer raskere og reduser databruken"
"Optimaliser mediekvaliteten"
"Moderasjon og sikkerhet"
+ "Optimaliser bilder automatisk for raskere opplastinger og mindre filstørrelser."
+ "Optimaliser kvaliteten på bildeopplasting"
+ "%1$s. Trykk her for å endre."
+ "Høy (1080p)"
+ "Lav (480p)"
+ "Standard (720p)"
+ "Kvalitet på videoopplasting"
"Leverandør av pushvarsling"
"Deaktiver rik tekstredigering for å skrive Markdown manuelt."
"Lesebekreftelser"
@@ -36,6 +44,11 @@
"Kan ikke oppdatere profilen"
"Rediger profil"
"Oppdaterer profilen…"
+ "Aktiver trådsvar"
+ "Appen vil starte på nytt for å implementere denne endringen."
+ "Prøv ut våre nyeste ideer under utvikling. Disse funksjonene er ikke ferdig utviklet; de kan være ustabile og kan endres."
+ "Lyst til å prøve noe nytt?"
+ "Prøvefunksjoner"
"Ytterligere innstillinger"
"Lyd- og videosamtaler"
"Uoverensstemmelse i konfigurasjonen"
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 f80947d2f7..8ece6bde9e 100644
--- a/features/preferences/impl/src/main/res/values-pt/translations.xml
+++ b/features/preferences/impl/src/main/res/values-pt/translations.xml
@@ -70,7 +70,7 @@ Se prosseguires, algumas delas podem ser alteradas."
"configurações do sistema"
"Notificações do sistema desativadas"
"Notificações"
- "Histórico de notificações"
+ "Histórico de push"
"Resolução de problemas"
"Corrigir notificações"
diff --git a/features/preferences/impl/src/main/res/values-ro/translations.xml b/features/preferences/impl/src/main/res/values-ro/translations.xml
index 5977771ee6..6f8e41c5b9 100644
--- a/features/preferences/impl/src/main/res/values-ro/translations.xml
+++ b/features/preferences/impl/src/main/res/values-ro/translations.xml
@@ -10,6 +10,7 @@
"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"
+ "Laboratoare"
"Încărcați fotografii și videoclipuri mai rapid și reduceți consumul de date"
"Optimizați calitatea media"
"Moderare și siguranță"
@@ -43,6 +44,11 @@
"Nu s-a putut actualiza profilul"
"Editați profilul"
"Se actualizează profilul…"
+ "Activați răspunsurile în fir"
+ "Aplicația va reporni pentru a aplica această modificare."
+ "Încercați cele mai noi idei în curs de dezvoltare. Aceste funcții nu sunt finalizate; pot fi instabile și pot suferi modificări."
+ "Doriți experiențe noi?"
+ "Laboratoare"
"Setări adiționale"
"Apeluri audio și video"
"Nepotrivire de configurație"
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 59d19b0210..6ac5a5d03c 100644
--- a/features/preferences/impl/src/main/res/values-ru/translations.xml
+++ b/features/preferences/impl/src/main/res/values-ru/translations.xml
@@ -10,9 +10,13 @@
"Адрес указан неверно, удостоверьтесь, что вы указали протокол (http/https) и правильный адрес."
"Скрыть аватары в запросах на приглашение в комнату"
"Скрыть предварительный просмотр медиафайлов на временной шкале"
+ "Лаборатория"
"Загружайте фотографии и видео быстрее и сокращайте потребление трафика"
"Оптимизировать качество мультимедиа"
"Модерация и безопасность"
+ "Автоматически оптимизируйте изображения для более быстрой загрузки и уменьшения размера файлов."
+ "Оптимизируйте качество загрузки изображения"
+ "%1$s. Нажмите здесь, чтобы изменить."
"Высокое (1080p)"
"Низкое (480p)"
"Среднее (720p)"
@@ -40,6 +44,10 @@
"Невозможно обновить профиль"
"Редактировать профиль"
"Обновление профиля…"
+ "Включить ответы в топике"
+ "Попробуйте наши последние идеи в разработке. Эти функции ещё не завершены, они могут быть нестабильны и могут измениться."
+ "Хотите попробовать?"
+ "Лаборатория"
"Дополнительные параметры"
"Аудио и видео звонки"
"Несоответствие конфигурации"
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 d9f5bb3c88..7ac8952090 100644
--- a/features/preferences/impl/src/main/res/values-zh/translations.xml
+++ b/features/preferences/impl/src/main/res/values-zh/translations.xml
@@ -10,6 +10,7 @@
"URL 无效,请确保包含协议(http/https)和正确的地址。"
"在房间邀请请求中隐藏头像"
"在时间轴中隐藏媒体预览"
+ "实验室"
"针对上传进行优化"
"媒体"
"内容审核与安全"
@@ -43,6 +44,7 @@
"无法更新个人资料"
"编辑个人资料"
"更新个人资料……"
+ "实验室"
"更多设置"
"音视频通话"
"配置不匹配"
diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml
index 8b3c9d1d82..31ee676226 100644
--- a/features/preferences/impl/src/main/res/values/localazy.xml
+++ b/features/preferences/impl/src/main/res/values/localazy.xml
@@ -10,6 +10,7 @@
"Invalid URL, please make sure you include the protocol (http/https) and the correct address."
"Hide avatars in room invite requests"
"Hide media previews in timeline"
+ "Labs"
"Upload photos and videos faster and reduce data usage"
"Optimise media quality"
"Moderation and Safety"
@@ -43,6 +44,11 @@
"Unable to update profile"
"Edit profile"
"Updating profile…"
+ "Enable thread replies"
+ "The app will restart to apply this change."
+ "Try out our latest ideas in development. These features are not finalised; they may be unstable, may change."
+ "Feeling experimental?"
+ "Labs"
"Additional settings"
"Audio and video calls"
"Configuration mismatch"
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
index 807ff3a5c6..9e1bd70376 100644
--- 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
@@ -20,7 +20,6 @@ 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.matrix.api.core.SessionId
import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint
import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError
@@ -64,10 +63,11 @@ class DefaultPreferencesEntryPointTest {
)
}
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(sessionId: SessionId, roomId: RoomId, eventId: EventId) = lambdaError()
+ override fun navigateTo(roomId: RoomId, eventId: EventId) = lambdaError()
}
val params = PreferencesEntryPoint.Params(
initialElement = PreferencesEntryPoint.InitialTarget.NotificationSettings,
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt
index 9e2c8ca175..76b22871f0 100644
--- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt
+++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt
@@ -19,6 +19,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.featureflag.api.FeatureFlags
+import io.element.android.libraries.featureflag.test.FakeFeature
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
@@ -35,7 +36,16 @@ class DeveloperSettingsPresenterTest {
@Test
fun `present - ensures initial states are correct`() = runTest {
- val presenter = createDeveloperSettingsPresenter()
+ val availableFeatures = listOf(
+ FakeFeature(
+ key = "feature_1",
+ title = "Feature 1",
+ isInLabs = false,
+ )
+ )
+ val presenter = createDeveloperSettingsPresenter(
+ featureFlagService = FakeFeatureFlagService(providedAvailableFeatures = availableFeatures)
+ )
presenter.test {
awaitItem().also { state ->
assertThat(state.features).isEmpty()
@@ -50,8 +60,7 @@ class DeveloperSettingsPresenterTest {
}
awaitItem().also { state ->
assertThat(state.features).isNotEmpty()
- val numberOfModifiableFeatureFlags = FeatureFlags.entries.count { it.isFinished.not() }
- assertThat(state.features).hasSize(numberOfModifiableFeatureFlags)
+ assertThat(state.features).hasSize(1)
assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.INFO)
}
awaitItem().also { state ->
@@ -161,8 +170,51 @@ class DeveloperSettingsPresenterTest {
}
}
+ @Test
+ fun `present - won't display features in labs or finished`() = runTest {
+ val availableFeatures = listOf(
+ // Only this feature should be displayed
+ FakeFeature(
+ key = "feature_1",
+ title = "Feature 1",
+ isInLabs = false,
+ ),
+ FakeFeature(
+ key = "feature_2",
+ title = "Feature 2",
+ isInLabs = true,
+ ),
+ FakeFeature(
+ key = "feature_3",
+ title = "Feature 3",
+ isInLabs = false,
+ isFinished = true,
+ )
+ )
+
+ val presenter = createDeveloperSettingsPresenter(
+ featureFlagService = FakeFeatureFlagService(
+ providedAvailableFeatures = availableFeatures,
+ )
+ )
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.features).hasSize(1)
+ }
+ }
+ }
+
private fun createDeveloperSettingsPresenter(
- featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
+ featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(
+ providedAvailableFeatures = listOf(
+ FakeFeature(
+ key = "feature_1",
+ title = "Feature 1",
+ isInLabs = false,
+ )
+ )
+ ),
cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(),
clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(),
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/labs/LabsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/labs/LabsPresenterTest.kt
new file mode 100644
index 0000000000..5117a9fe94
--- /dev/null
+++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/labs/LabsPresenterTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: 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.labs
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
+import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
+import io.element.android.libraries.featureflag.api.Feature
+import io.element.android.libraries.featureflag.api.FeatureFlags
+import io.element.android.libraries.featureflag.test.FakeFeature
+import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
+import io.element.android.services.toolbox.test.strings.FakeStringProvider
+import io.element.android.tests.testutils.test
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class LabsPresenterTest {
+ @Test
+ fun `present - ensures only unfinished features in labs are displayed`() = runTest {
+ val availableFeatures = listOf(
+ FakeFeature(
+ key = "feature_1",
+ title = "Feature 1",
+ isInLabs = true,
+ ),
+ FakeFeature(
+ key = "feature_2",
+ title = "Feature 2",
+ isInLabs = false,
+ ),
+ FakeFeature(
+ key = "feature_3",
+ title = "Feature 3",
+ isInLabs = true,
+ isFinished = true,
+ )
+ )
+ createLabsPresenter(
+ availableFeatures = availableFeatures,
+ ).test {
+ val receivedFeatures = awaitItem().features
+ assertThat(receivedFeatures).hasSize(1)
+ assertThat(receivedFeatures.first().key).isEqualTo(availableFeatures.first().key)
+
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - ToggleFeature actually toggles the value`() = runTest {
+ val availableFeatures = listOf(
+ FakeFeature(
+ key = "feature_1",
+ title = "Feature 1",
+ isInLabs = true,
+ ),
+ )
+ createLabsPresenter(
+ availableFeatures = availableFeatures,
+ ).test {
+ val initialItem = awaitItem()
+ val feature = initialItem.features.first()
+ assertThat(feature.isEnabled).isFalse()
+
+ // Wait until the data finished loading
+ skipItems(1)
+
+ // Toggle the feature, should be true now
+ initialItem.eventSink(LabsEvents.ToggleFeature(feature))
+ assertThat(awaitItem().features.first().isEnabled).isTrue()
+
+ // Toggle the feature, should be false now
+ initialItem.eventSink(LabsEvents.ToggleFeature(feature))
+ assertThat(awaitItem().features.first().isEnabled).isFalse()
+ }
+ }
+
+ @Test
+ fun `present - ToggleFeature with the 'Threads' feature resets the cache`() = runTest {
+ val availableFeatures = listOf(
+ FakeFeature(
+ key = FeatureFlags.Threads.key,
+ title = "Threads",
+ isInLabs = true,
+ ),
+ )
+
+ val clearCacheUseCase = FakeClearCacheUseCase()
+ createLabsPresenter(
+ availableFeatures = availableFeatures,
+ clearCacheUseCase = clearCacheUseCase,
+ ).test {
+ val initialItem = awaitItem()
+ val feature = initialItem.features.first()
+ assertThat(feature.isEnabled).isFalse()
+ assertThat(initialItem.isApplyingChanges).isFalse()
+
+ // Wait until the data finished loading
+ skipItems(1)
+
+ // Toggle the feature
+ initialItem.eventSink(LabsEvents.ToggleFeature(feature))
+ assertThat(awaitItem().features.first().isEnabled).isTrue()
+
+ // The clear cache use case should have been called
+ assertThat(awaitItem().isApplyingChanges).isTrue()
+ assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
+ }
+ }
+
+ private fun createLabsPresenter(
+ availableFeatures: List = emptyList(),
+ clearCacheUseCase: ClearCacheUseCase = FakeClearCacheUseCase(),
+ ): LabsPresenter {
+ return LabsPresenter(
+ stringProvider = FakeStringProvider(),
+ featureFlagService = FakeFeatureFlagService(providedAvailableFeatures = availableFeatures),
+ clearCacheUseCase = clearCacheUseCase,
+ )
+ }
+}
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..f65589beb6 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,24 @@ 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.FakeFeature
+import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.indicator.api.IndicatorService
import io.element.android.libraries.indicator.test.FakeIndicatorService
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.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 +70,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 +185,80 @@ class PreferencesRootPresenterTest {
}
}
+ @Test
+ fun `present - labs can be shown if any feature flag is in labs and not finished`() = runTest {
+ createPresenter(
+ featureFlagService = FakeFeatureFlagService(
+ providedAvailableFeatures = listOf(
+ FakeFeature(
+ key = "feature_1",
+ title = "Feature 1",
+ isInLabs = true,
+ isFinished = false,
+ )
+ )
+ ),
+ matrixClient = FakeMatrixClient(
+ canDeactivateAccountResult = { true },
+ accountManagementUrlResult = { Result.success(null) },
+ ),
+ ).test {
+ assertThat(awaitItem().showLabsItem).isTrue()
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - labs can't be shown if all feature flags in labs are finished`() = runTest {
+ createPresenter(
+ featureFlagService = FakeFeatureFlagService(
+ providedAvailableFeatures = listOf(
+ FakeFeature(
+ key = "feature_1",
+ title = "Feature 1",
+ isInLabs = true,
+ isFinished = true,
+ )
+ )
+ ),
+ matrixClient = FakeMatrixClient(
+ canDeactivateAccountResult = { true },
+ accountManagementUrlResult = { Result.success(null) },
+ ),
+ ).test {
+ assertThat(awaitItem().showLabsItem).isFalse()
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @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 +270,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 +282,7 @@ class PreferencesRootPresenterTest {
directLogoutPresenter = { aDirectLogoutState() },
showDeveloperSettingsProvider = showDeveloperSettingsProvider,
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
+ featureFlagService = featureFlagService,
+ sessionStore = sessionStore,
)
}
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/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
index 0dd4d4f518..10af89f740 100644
--- 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
@@ -19,7 +19,7 @@ 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.Inject
+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
@@ -29,7 +29,7 @@ import io.element.android.libraries.architecture.createNode
import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class BugReportFlowNode(
@Assisted val buildContext: BuildContext,
@Assisted plugins: List,
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 b6eb494589..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
@@ -16,14 +16,14 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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.ui.strings.CommonStrings
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class BugReportNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 01b6857c0b..5787939688 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
@@ -176,8 +176,8 @@ class DefaultBugReporter(
.addFormDataPart("branch_name", buildMeta.gitBranchName)
userId?.let {
matrixClientProvider.getOrNull(it)?.let { client ->
- val curveKey = client.encryptionService().deviceCurve25519()
- val edKey = client.encryptionService().deviceEd25519()
+ val curveKey = client.encryptionService.deviceCurve25519()
+ val edKey = client.encryptionService.deviceEd25519()
if (curveKey != null && edKey != null) {
builder.addFormDataPart("device_keys", "curve25519:$curveKey, ed25519:$edKey")
}
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-cy/translations.xml b/features/rageshake/impl/src/main/res/values-cy/translations.xml
index 123c23ef7f..c9cee768e9 100644
--- a/features/rageshake/impl/src/main/res/values-cy/translations.xml
+++ b/features/rageshake/impl/src/main/res/values-cy/translations.xml
@@ -14,5 +14,7 @@
"Anfon luniau sgrin"
"Bydd cofnodion yn cael eu cynnwys gyda\'ch neges i wneud yn siŵr bod popeth yn gweithio\'n iawn. I anfon eich neges heb logiau, diffoddwch y gosodiad hwn."
"Chwalodd %1$s y tro diwethaf iddo gael ei ddefnyddio. Hoffech chi rannu adroddiad gwall gyda ni?"
+ "Os ydych chi\'n cael problemau gyda hysbysiadau, gall llwytho\'r gosodiadau hysbysiadau ein helpu i ddarganfod yr achos gwreiddiol."
+ "Anfon gosodiadau hysbysiadau"
"Gweld logiau"
diff --git a/features/rageshake/impl/src/main/res/values-da/translations.xml b/features/rageshake/impl/src/main/res/values-da/translations.xml
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 85893afa54..33baf29d59 100644
--- a/features/rageshake/impl/src/main/res/values-de/translations.xml
+++ b/features/rageshake/impl/src/main/res/values-de/translations.xml
@@ -14,5 +14,7 @@
"Bildschirmfoto senden"
"Die Protokolle werden deiner Nachricht beigefügt, um sicherzustellen, dass alles ordnungsgemäß funktioniert. Um deine Nachricht ohne Protokolle zu senden, deaktiviere diese Einstellung."
"%1$s ist bei der letzten Nutzung abgestürzt. Möchtest du einen Absturzbericht mit uns teilen?"
+ "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-fi/translations.xml b/features/rageshake/impl/src/main/res/values-fi/translations.xml
index 38f30d4289..1970d538fd 100644
--- a/features/rageshake/impl/src/main/res/values-fi/translations.xml
+++ b/features/rageshake/impl/src/main/res/values-fi/translations.xml
@@ -14,5 +14,7 @@
"Lähetä kuvakaappaus"
"Lähetä lokitiedostot viestisi kanssa, jotta voimme varmistaa, että kaikki toimii oikein. Jos haluat lähettää viestisi ilman lokeja, jätä tämä asetus valitsematta."
"%1$s kaatui edellisellä käyttökerralla. Haluatko jakaa virheraportin kanssamme?"
+ "Jos sinulla on ongelmia ilmoitusten kanssa, ilmoitusasetusten lähettäminen voi auttaa meitä selvittämään ongelman syyn."
+ "Lähetä ilmoitusasetukset"
"Näytä lokitiedostot"
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-nb/translations.xml b/features/rageshake/impl/src/main/res/values-nb/translations.xml
index 277effef92..4fa4d0a653 100644
--- a/features/rageshake/impl/src/main/res/values-nb/translations.xml
+++ b/features/rageshake/impl/src/main/res/values-nb/translations.xml
@@ -14,5 +14,6 @@
"Send skjermbilde"
"Logger vil bli inkludert i meldingen din, for å sikre at alt fungerer som det skal. For å sende meldingen uten logger, slå av denne innstillingen."
"%1$s krasjet sist gang den ble brukt. Vil du dele en krasjrapport med oss?"
+ "Hvis du har problemer med varsler, kan det å laste opp varslingsinnstillingene hjelpe oss med å finne den underliggende årsaken."
"Vis logger"
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 6c2a7cb611..f342ffd4c5 100644
--- a/features/rageshake/impl/src/main/res/values-ro/translations.xml
+++ b/features/rageshake/impl/src/main/res/values-ro/translations.xml
@@ -14,5 +14,7 @@
"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?"
+ "Dacă întâmpinați probleme cu notificările, încărcarea setărilor notificărilor ne poate ajuta să identificăm cauza principală."
+ "Trimiteți setările notificărilor"
"Vizualizați log-urile"
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 5a8f2ceb71..f1d5216405 100644
--- a/features/rageshake/impl/src/main/res/values-ru/translations.xml
+++ b/features/rageshake/impl/src/main/res/values-ru/translations.xml
@@ -14,5 +14,7 @@
"Отправить снимок экрана"
"Чтобы убедиться, что все работает правильно, в сообщение будут включены журналы. Чтобы отправить сообщение без журналов, отключите эту настройку."
"При последнем использовании %1$s произошел сбой. Хотите поделиться отчетом о сбое?"
+ "Если у вас возникли проблемы с уведомлениями, загрузка настроек уведомлений может помочь нам определить основную причину."
+ "Настройки отправки уведомлений"
"Просмотр журналов"
diff --git a/features/rageshake/impl/src/main/res/values-zh/translations.xml b/features/rageshake/impl/src/main/res/values-zh/translations.xml
index 80125d638a..527a35cdcc 100644
--- a/features/rageshake/impl/src/main/res/values-zh/translations.xml
+++ b/features/rageshake/impl/src/main/res/values-zh/translations.xml
@@ -14,5 +14,7 @@
"发送屏幕截图"
"为确认一切正常运行,您的消息中将包含日志。如要发送不带日志的消息,请关闭此设置。"
"%1$s 上次使用时崩溃了。想和我们分享崩溃报告吗?"
+ "如果您遇到通知问题,上传通知设置可以帮助我们查明根本原因。"
+ "发送通知设置"
"查看日志"
diff --git a/features/rageshake/impl/src/main/res/values/localazy.xml b/features/rageshake/impl/src/main/res/values/localazy.xml
index 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/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 917b081a3d..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
@@ -13,7 +13,7 @@ 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.Inject
+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
@@ -21,7 +21,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class ReportRoomNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 f47ab19004..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
@@ -17,7 +17,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 +25,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-@Inject
+@AssistedInject
class ReportRoomPresenter(
@Assisted private val roomId: RoomId,
private val reportRoom: ReportRoom,
diff --git a/features/reportroom/impl/src/main/res/values-fr/translations.xml b/features/reportroom/impl/src/main/res/values-fr/translations.xml
index 51376e34ea..4936a4d54f 100644
--- a/features/reportroom/impl/src/main/res/values-fr/translations.xml
+++ b/features/reportroom/impl/src/main/res/values-fr/translations.xml
@@ -3,6 +3,6 @@
"Votre rapport a été envoyé avec succès, mais nous avons rencontré un problème en essayant de quitter le salon. Veuillez réessayer."
"Impossible de quitter le salon"
"Signaler ce salon à votre admin. Si les messages sont chiffrés, votre admin ne pourra pas les lire."
- "Décrivez la raison…"
+ "Décrivez la raison du signalement…"
"Signaler le salon"
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 274ccef7c8..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
@@ -14,7 +14,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -22,7 +22,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class RoomAliasResolverNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 9b65d3e14a..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
@@ -14,7 +14,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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,7 +25,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.jvm.optionals.getOrElse
-@Inject
+@AssistedInject
class RoomAliasResolverPresenter(
@Assisted private val roomAlias: RoomAlias,
private val matrixClient: MatrixClient,
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 c883613203..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
@@ -21,7 +21,7 @@ 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.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.annotations.ContributesNode
import io.element.android.appconfig.LearnMoreConfig
@@ -67,7 +67,7 @@ import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class RoomDetailsFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 6eb8b81d34..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
@@ -20,7 +20,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.features.leaveroom.api.LeaveRoomRenderer
@@ -36,7 +36,7 @@ import timber.log.Timber
import io.element.android.libraries.androidutils.R as AndroidUtilsR
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class RoomDetailsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 0071aced5d..4408deab1e 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
@@ -51,7 +51,7 @@ import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -143,12 +143,12 @@ class RoomDetailsPresenter(
}
RoomDetailsEvent.MuteNotification -> {
scope.launch(dispatchers.io) {
- client.notificationSettingsService().muteRoom(room.roomId)
+ notificationSettingsService.muteRoom(room.roomId)
}
}
RoomDetailsEvent.UnmuteNotification -> {
scope.launch(dispatchers.io) {
- client.notificationSettingsService().unmuteRoom(room.roomId, isEncrypted, room.isOneToOne)
+ notificationSettingsService.unmuteRoom(room.roomId, isEncrypted, room.isOneToOne)
}
}
is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite)
@@ -194,7 +194,7 @@ class RoomDetailsPresenter(
isFavorite = isFavorite,
displayRolesAndPermissionsSettings = !isDm && isUserAdmin,
isPublic = joinRule == JoinRule.Public,
- heroes = roomInfo.heroes.toPersistentList(),
+ heroes = roomInfo.heroes.toImmutableList(),
pinnedMessagesCount = pinnedMessagesCount,
snackbarMessage = snackbarMessage,
canShowKnockRequests = canShowKnockRequests,
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt
index 70810ee46b..43566b86a8 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt
@@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
data class RoomDetailsState(
val roomId: RoomId,
@@ -59,7 +59,7 @@ data class RoomDetailsState(
if (isPublic) {
add(RoomBadge.PUBLIC)
}
- }.toPersistentList()
+ }.toImmutableList()
}
@Immutable
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt
index c9087d6458..96d19c5ecf 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
@@ -27,7 +27,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
open class RoomDetailsStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -135,7 +135,7 @@ fun aRoomDetailsState(
isFavorite = isFavorite,
displayRolesAndPermissionsSettings = displayAdminSettings,
isPublic = isPublic,
- heroes = heroes.toPersistentList(),
+ heroes = heroes.toImmutableList(),
pinnedMessagesCount = pinnedMessagesCount,
snackbarMessage = snackbarMessage,
canShowKnockRequests = canShowKnockRequests,
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 61d5ba115a..3f20985ab8 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
@@ -89,7 +89,6 @@ import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
-import kotlinx.collections.immutable.toPersistentList
@Composable
fun RoomDetailsView(
@@ -400,7 +399,7 @@ private fun RoomHeaderSection(
avatarType = AvatarType.Room(
heroes = heroes.map { user ->
user.getAvatarData(size = AvatarSize.RoomDetailsHeader)
- }.toPersistentList(),
+ }.toImmutableList(),
isTombstoned = isTombstoned,
),
contentDescription = avatarUrl?.let { stringResource(CommonStrings.a11y_room_avatar) },
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 f04a50c657..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
@@ -14,14 +14,14 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.RoomScope
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class RoomDetailsEditNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 04b98e3d98..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
@@ -15,7 +15,7 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
@@ -25,7 +25,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class RoomInviteMembersNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 178410d264..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
@@ -15,7 +15,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.features.roommembermoderation.api.ModerationAction
@@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class RoomMemberListNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 c5bd507d69..69efd04b9e 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
@@ -38,7 +38,7 @@ import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
-import kotlinx.collections.immutable.toPersistentMap
+import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
@@ -69,10 +69,10 @@ class RoomMemberListPresenter(
val canInvite by room.canInviteAsState(syncUpdateFlow.value)
val roomModerationState = roomMembersModerationPresenter.present()
- val roomMemberIdentityStates by produceState(persistentMapOf()) {
+ val roomMemberIdentityStates by produceState(persistentMapOf()) {
room.roomMemberIdentityStateChange(waitForEncryption = true)
.onEach { identities ->
- value = identities.associateBy({ it.identityRoomMember.userId }, { it.identityState }).toPersistentMap()
+ value = identities.associateBy({ it.identityRoomMember.userId }, { it.identityState }).toImmutableMap()
}
.launchIn(this)
}
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 35d386976f..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
@@ -15,7 +15,7 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
@@ -29,7 +29,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class RoomMemberDetailsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 61c1eb9019..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
@@ -15,7 +15,7 @@ import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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,7 @@ import kotlinx.coroutines.launch
* Presenter for room member details screen.
* Rely on UserProfilePresenter, but override some fields with room member info when available.
*/
-@Inject
+@AssistedInject
class RoomMemberDetailsPresenter(
@Assisted private val roomMemberId: UserId,
private val room: JoinedRoom,
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 7ef6ea9c1c..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
@@ -15,7 +15,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
@@ -24,7 +24,7 @@ import io.element.android.libraries.di.RoomScope
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class RoomNotificationSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 93e22a9d9e..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
@@ -19,7 +19,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
-@Inject
+@AssistedInject
class RoomNotificationSettingsPresenter(
private val room: JoinedRoom,
private val notificationSettingsService: NotificationSettingsService,
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 61bb0e99a3..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
@@ -18,7 +18,7 @@ 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.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -33,7 +33,7 @@ import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class RolesAndPermissionsFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 230e83e660..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
@@ -16,7 +16,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class RolesAndPermissionsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 906c97fb2e..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
@@ -14,7 +14,7 @@ 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.Inject
+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
@@ -22,7 +22,7 @@ import io.element.android.libraries.di.RoomScope
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class ChangeRoomPermissionsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 f010fd6057..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
@@ -17,7 +17,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-@Inject
+@AssistedInject
class ChangeRoomPermissionsPresenter(
@Assisted private val section: ChangeRoomPermissionsSection,
private val room: JoinedRoom,
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsStateProvider.kt
index 7af850230e..07d3455a90 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsStateProvider.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsStateProvider.kt
@@ -11,7 +11,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
class ChangeRoomPermissionsStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -45,7 +45,7 @@ internal fun aChangeRoomPermissionsState(
) = ChangeRoomPermissionsState(
section = section,
currentPermissions = currentPermissions,
- items = items.toPersistentList(),
+ items = items.toImmutableList(),
hasChanges = hasChanges,
saveAction = saveAction,
confirmExitAction = confirmExitAction,
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 2744834c73..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
@@ -15,7 +15,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -25,7 +25,7 @@ import io.element.android.libraries.di.RoomScope
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class SecurityAndPrivacyFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 ffbf58af1a..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
@@ -14,12 +14,12 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.RoomScope
@ContributesNode(RoomScope::class)
-@Inject
+@AssistedInject
class SecurityAndPrivacyNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 a2a55aed68..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
@@ -19,7 +19,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-@Inject
+@AssistedInject
class SecurityAndPrivacyPresenter(
@Assisted private val navigator: SecurityAndPrivacyNavigator,
private val matrixClient: MatrixClient,
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 943400d4c3..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
@@ -14,13 +14,13 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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)
-@Inject
+@AssistedInject
class EditRoomAddressNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 db02c60346..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
@@ -18,7 +18,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-@Inject
+@AssistedInject
class EditRoomAddressPresenter(
@Assisted private val navigator: SecurityAndPrivacyNavigator,
private val client: MatrixClient,
diff --git a/features/roomdetails/impl/src/main/res/values-be/translations.xml b/features/roomdetails/impl/src/main/res/values-be/translations.xml
index 01c076fdb5..8ab30212b7 100644
--- a/features/roomdetails/impl/src/main/res/values-be/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-be/translations.xml
@@ -51,6 +51,7 @@
"Стандартныя"
"Апавяшчэнні"
"Замацаваныя паведамленні"
+ "Профіль"
"Ролі і дазволы"
"Назва пакоя"
"Бяспека"
diff --git a/features/roomdetails/impl/src/main/res/values-bg/translations.xml b/features/roomdetails/impl/src/main/res/values-bg/translations.xml
index 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-da/translations.xml b/features/roomdetails/impl/src/main/res/values-da/translations.xml
index 29733beeed..77e34d87dc 100644
--- a/features/roomdetails/impl/src/main/res/values-da/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-da/translations.xml
@@ -50,6 +50,8 @@
"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."
+ "Luk ikke appen, før den er færdig."
+ "Forbereder invitationer…"
"Invitér andre"
"Forlad samtalen"
"Forlad rum"
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 feea21919d..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,7 @@
"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"
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-ka/translations.xml b/features/roomdetails/impl/src/main/res/values-ka/translations.xml
index 1beb70d4dd..481082bf63 100644
--- a/features/roomdetails/impl/src/main/res/values-ka/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-ka/translations.xml
@@ -46,6 +46,7 @@
"მორგებული"
"ნაგულისხმევი"
"შეტყობინებები"
+ "პროფილი"
"როლები და ნებართვები"
"ოთახის სახელი"
"უსაფრთხოება"
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 596b387b04..16042fdf57 100644
--- a/features/roomdetails/impl/src/main/res/values-nb/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-nb/translations.xml
@@ -22,6 +22,7 @@
"Rediger administratorer"
"Du vil ikke kunne angre denne handlingen. Du forfremmer brukeren til å ha samme rettighetsnivå som deg."
"Legg til administrator?"
+ "Du kan ikke angre denne handlingen. Du overfører eierskapet til de valgte brukerne. Når du forlater siden, vil dette være permanent."
"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."
@@ -49,6 +50,8 @@
"Det oppstod en feil ved lasting av varslingsinnstillinger."
"Mislyktes i å dempe dette rommet, prøv igjen."
"Mislyktes i å oppheve dempingen av dette rommet, prøv igjen."
+ "Ikke lukk appen før den er ferdig."
+ "Forbereder invitasjoner…"
"Inviter folk"
"Forlat samtalen"
"Forlat rommet"
diff --git a/features/roomdetails/impl/src/main/res/values-nl/translations.xml b/features/roomdetails/impl/src/main/res/values-nl/translations.xml
index 9b932f60fe..baec0b0bb9 100644
--- a/features/roomdetails/impl/src/main/res/values-nl/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-nl/translations.xml
@@ -51,6 +51,7 @@
"Standaard"
"Meldingen"
"Vastgezette berichten"
+ "Profiel"
"Rollen en rechten"
"Naam van de kamer"
"Beveiliging"
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 fbdf3132a4..7379a45550 100644
--- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml
@@ -50,6 +50,8 @@
"A apărut o eroare la încărcarea setărilor pentru notificari."
"Dezactivarea notificarilor pentru această cameră a eșuat, încercați din nou."
"Activarea notificarilor pentru această cameră a eșuat, încercați din nou."
+ "Nu închideți aplicația până nu se termină."
+ "Se pregătesc invitațiile…"
"Invitați prieteni"
"Părăsiți conversația"
"Părăsiți camera"
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 e24adbd6c1..82efd71adf 100644
--- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml
@@ -50,6 +50,8 @@
"Произошла ошибка при загрузке настроек уведомлений."
"Не удалось отключить звук в этой комнате, попробуйте еще раз."
"Не удалось включить звук в эту комнату, попробуйте еще раз."
+ "Не закрывайте приложение, пока не закончите."
+ "Подготовка приглашений…"
"Пригласить в комнату"
"Покинуть беседу"
"Покинуть комнату"
diff --git a/features/roomdetails/impl/src/main/res/values-ur/translations.xml b/features/roomdetails/impl/src/main/res/values-ur/translations.xml
index 12a8d964cb..8cd040835f 100644
--- a/features/roomdetails/impl/src/main/res/values-ur/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-ur/translations.xml
@@ -51,6 +51,7 @@
"طے شدہ"
"اطلاعات"
"مثبوتہ پیغامات"
+ "نمایہ"
"کردارہا اور اجازتیں"
"کمرے کا نام"
"حفاظت"
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 9ced40ed1f..e9b50e5400 100644
--- a/features/roomdetails/impl/src/main/res/values-uz/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-uz/translations.xml
@@ -51,6 +51,7 @@
"Standart"
"Bildirishnomalar"
"Qadalgan xabarlar"
+ "Profil"
"Rollar va ruxsatlar"
"Xona nomi"
"Xavfsizlik"
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 035de6fa4d..888701406d 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
@@ -60,7 +60,7 @@
"預設"
"通知"
"釘選訊息"
- "設定檔"
+ "個人檔案"
"請求加入"
"身份與權限"
"聊天室名稱"
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 48a92f93a2..b260163ca3 100644
--- a/features/roomdetails/impl/src/main/res/values-zh/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-zh/translations.xml
@@ -50,6 +50,8 @@
"加载通知设置时出错。"
"无法将此聊天室静音,请重试。"
"无法取消此聊天室的静音,请重试。"
+ "完成之前请勿关闭应用程序。"
+ "准备邀请…"
"邀请朋友"
"离开聊天"
"离开聊天室"
@@ -58,7 +60,7 @@
"默认"
"通知"
"置顶消息"
- "简介"
+ "个人资料"
"申请加入"
"角色与权限"
"聊天室名称"
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt
index bfcfa184cb..695168aa16 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt
@@ -104,7 +104,7 @@ class RoomDetailsPresenterTest {
client = matrixClient,
room = room,
featureFlagService = featureFlagService,
- notificationSettingsService = matrixClient.notificationSettingsService(),
+ notificationSettingsService = matrixClient.notificationSettingsService,
roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory,
leaveRoomPresenter = { leaveRoomState },
roomCallStatePresenter = { aStandByCallState() },
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 50f50f8d4e..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
@@ -14,14 +14,14 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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)
-@Inject
+@AssistedInject
class RoomDirectoryNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt
index bdebcc3a93..2d4cc75aa9 100644
--- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt
+++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt
@@ -14,7 +14,7 @@ import io.element.android.features.roommembermoderation.api.RoomMemberModeration
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
class InternalRoomMemberModerationStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -86,7 +86,7 @@ fun aRoomMembersModerationState(
canKick = canKick,
canBan = canBan,
selectedUser = selectedUser,
- actions = actions.toPersistentList(),
+ actions = actions.toImmutableList(),
kickUserAsyncAction = kickUserAsyncAction,
banUserAsyncAction = banUserAsyncAction,
unbanUserAsyncAction = unbanUserAsyncAction,
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 3d07ee75e1..2ddb3ad34a 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
@@ -35,9 +35,9 @@ import io.element.android.libraries.matrix.ui.room.canBanAsState
import io.element.android.libraries.matrix.ui.room.canKickAsState
import io.element.android.libraries.matrix.ui.room.userPowerLevelAsState
import io.element.android.services.analytics.api.AnalyticsService
-import kotlinx.collections.immutable.PersistentList
+import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.drop
@@ -68,7 +68,7 @@ class RoomMemberModerationPresenter(
var selectedUser by remember {
mutableStateOf(null)
}
- val moderationActions = remember { mutableStateOf(persistentListOf()) }
+ val moderationActions = remember { mutableStateOf>(persistentListOf()) }
fun handleEvent(event: RoomMemberModerationEvents) {
when (event) {
@@ -149,7 +149,7 @@ class RoomMemberModerationPresenter(
canKick: Boolean,
canBan: Boolean,
currentUserMemberPowerLevel: Long,
- ): PersistentList {
+ ): ImmutableList {
return buildList {
add(ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true))
// Assume the member is a regular user when it's unknown
@@ -168,7 +168,7 @@ class RoomMemberModerationPresenter(
add(ModerationActionState(action = ModerationAction.BanUser, isEnabled = canModerateThisUser))
}
}
- }.toPersistentList()
+ }.toImmutableList()
}
private fun CoroutineScope.kickUser(
diff --git a/features/roommembermoderation/impl/src/main/res/values-be/translations.xml b/features/roommembermoderation/impl/src/main/res/values-be/translations.xml
index bd744e96e7..b4b1f35f18 100644
--- a/features/roommembermoderation/impl/src/main/res/values-be/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-be/translations.xml
@@ -5,8 +5,11 @@
"Яны не змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць."
"Вы ўпэўнены, што хочаце заблакіраваць гэтага карыстальніка?"
"Блакіроўка %1$s"
+ "Яны змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць."
"Прагляд профілю"
"Выдаліць удзельніка з пакоя"
"Выдаліць удзельніка і забараніць далучацца ў будучыні?"
"Выдаленне %1$s…"
+ "Разблакіраваць"
+ "Разблакіроўка %1$s"
diff --git a/features/roommembermoderation/impl/src/main/res/values-cs/translations.xml b/features/roommembermoderation/impl/src/main/res/values-cs/translations.xml
index 2c5e51972e..263aa9a2e0 100644
--- a/features/roommembermoderation/impl/src/main/res/values-cs/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-cs/translations.xml
@@ -6,7 +6,7 @@
"Jste si jisti, že chcete vykázat tohoto člena?"
"Vykazování %1$s"
"Odebrat"
- "Budou moci znovu vstoupit do této místnosti, pokud budou pozváni."
+ "Pokud budou pozváni, budou se moci do této místnosti znovu připojit."
"Opravdu chcete tohoto člena odebrat?"
"Zobrazit profil"
"Odebrat z místnosti"
diff --git a/features/roommembermoderation/impl/src/main/res/values-cy/translations.xml b/features/roommembermoderation/impl/src/main/res/values-cy/translations.xml
index cfa26c6e9b..2d7667aaa7 100644
--- a/features/roommembermoderation/impl/src/main/res/values-cy/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-cy/translations.xml
@@ -13,7 +13,7 @@
"Dileu aelod a\'u gwahardd rhag ymuno yn y dyfodol?"
"Wrthi\'n dileu %1$s…"
"Dad-wahardd o\'r ystafell"
- "Dad-wahardd"
+ "Adfer"
"Bydden nhw\'n gallu ymuno â\'r ystafell eto os fydd rhywun yn eu gwahodd"
"Ydych chi\'n siŵr eich bod chi eisiau dadwahardd yr aelod hwn?"
"Dad-wahardd %1$s"
diff --git a/features/roommembermoderation/impl/src/main/res/values-de/translations.xml b/features/roommembermoderation/impl/src/main/res/values-de/translations.xml
index d977d05564..6c6f9dc827 100644
--- a/features/roommembermoderation/impl/src/main/res/values-de/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-de/translations.xml
@@ -6,7 +6,7 @@
"Möchtest du diesen Nutzer wirklich sperren?"
"%1$s wird gesperrt."
"Entfernen"
- "Sie können diesem Chat auf Einladung wieder beitreten."
+ "Die Nutzer können dem Chat wieder beitreten, wenn sie eingeladen werden."
"Möchtest du dieses Mitglied wirklich entfernen?"
"Nutzerprofil anzeigen"
"Mitglied entfernen"
@@ -16,5 +16,5 @@
"Sperre 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"
+ "%1$s wird entsperrt."
diff --git a/features/roommembermoderation/impl/src/main/res/values-el/translations.xml b/features/roommembermoderation/impl/src/main/res/values-el/translations.xml
index 2ed8f03bc4..89bf97f4d2 100644
--- a/features/roommembermoderation/impl/src/main/res/values-el/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-el/translations.xml
@@ -13,7 +13,7 @@
"Αφαίρεση μέλους και απαγόρευση συμμετοχής στο μέλλον;"
"Αφαίρεση %1$s…"
"Άρση αποκλεισμού από την αίθουσα"
- "Άρση αποκλεισμού"
+ "Αναίρεση αποκλεισμού"
"Θα μπορούν να συμμετάσχουν και πάλι στην αίθουσα αν προσκληθούν"
"Σίγουρα θες να καταργήσεις τον αποκλεισμό αυτού του μέλους;"
"Άρση αποκλεισμού %1$s"
diff --git a/features/roommembermoderation/impl/src/main/res/values-es/translations.xml b/features/roommembermoderation/impl/src/main/res/values-es/translations.xml
index 04049e9e31..7f83f7546e 100644
--- a/features/roommembermoderation/impl/src/main/res/values-es/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-es/translations.xml
@@ -13,7 +13,7 @@
"¿Sacar al miembro y prohibirle unirse en el futuro?"
"Eliminando %1$s…"
"Eliminar veto en la sala"
- "Eliminar veto"
+ "Quitar veto"
"Podría volver a unirse a la sala si se le invita"
"¿Seguro que quieres levantarle el veto a este miembro?"
"Levantando veto a %1$s"
diff --git a/features/roommembermoderation/impl/src/main/res/values-et/translations.xml b/features/roommembermoderation/impl/src/main/res/values-et/translations.xml
index ffb1d79f88..1a0a3ec431 100644
--- a/features/roommembermoderation/impl/src/main/res/values-et/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-et/translations.xml
@@ -6,7 +6,7 @@
"Kas sa oled kindel, et soovid sellele kasutajale seada suhtluskeelu?"
"Seame kasutajale %1$s suhtluskeelu"
"Eemalda"
- "Uue kutse saamisel on tal võimalik selle jututoaga uuesti liituda."
+ "Kutse olemasolul saab ta nüüd jututoaga uuesti liituda"
"Kas sa oled kindel, et soovid selle osaleja eemaldada?"
"Vaata profiili"
"Eemalda kasutaja jututoast"
diff --git a/features/roommembermoderation/impl/src/main/res/values-eu/translations.xml b/features/roommembermoderation/impl/src/main/res/values-eu/translations.xml
index a7f61e1a55..560e0a6b64 100644
--- a/features/roommembermoderation/impl/src/main/res/values-eu/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-eu/translations.xml
@@ -9,4 +9,6 @@
"Kendu gelatik"
"Kidea kendu eta etorkizunean sartzea debekatu?"
"%1$s kentzen…"
+ "Kendu debekua"
+ "%1$s(r)i debekua kentzen"
diff --git a/features/roommembermoderation/impl/src/main/res/values-fr/translations.xml b/features/roommembermoderation/impl/src/main/res/values-fr/translations.xml
index 5a09dddf4e..fa01ddb49a 100644
--- a/features/roommembermoderation/impl/src/main/res/values-fr/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-fr/translations.xml
@@ -2,11 +2,11 @@
"Bannir du salon"
"Bannir"
- "Il ne pourra pas rejoindre le salon à nouveau, même si il est invité."
+ "Ce compte ne pourra pas rejoindre le salon à nouveau, même si il est invité."
"Êtes-vous certain de vouloir bannir ce membre ?"
"Bannissement de %1$s"
"Retirer"
- "Cet utilisateur pourra rejoindre le salon à nouveau si il est invité."
+ "Il pourra rejoindre le salon à nouveau si il est invité."
"Voulez-vous vraiment supprimer ce membre ?"
"Voir le profil"
"Retirer le membre du salon"
@@ -14,7 +14,7 @@
"Enlever %1$s…"
"Débannir du salon"
"Débannir"
- "L’utilisateur pourra à nouveau rejoindre le salon s’il est invité."
- "Êtes-vous sûr de vouloir débannir cet utilisateur?"
+ "Ce compte pourra à nouveau rejoindre le salon s’il est invité."
+ "Êtes-vous sûr de vouloir débannir ce compte?"
"Débannissement de %1$s"
diff --git a/features/roommembermoderation/impl/src/main/res/values-hu/translations.xml b/features/roommembermoderation/impl/src/main/res/values-hu/translations.xml
index db36f41964..4c68806e7d 100644
--- a/features/roommembermoderation/impl/src/main/res/values-hu/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-hu/translations.xml
@@ -13,8 +13,8 @@
"Eltávolítja a tagot, és megtiltja a jövőbeni csatlakozást?"
"%1$s eltávolítása…"
"Visszaengedés a szobába"
- "Kitiltás visszavonása"
+ "Tiltás feloldása"
"Újra beléphetnek a szobába, ha meghívják őket."
"Biztos, hogy feloldja a felhasználó kitiltását?"
- "%1$s kitiltásának feloldása"
+ "%1$s tiltásának feloldása"
diff --git a/features/roommembermoderation/impl/src/main/res/values-in/translations.xml b/features/roommembermoderation/impl/src/main/res/values-in/translations.xml
index 778f68402c..bddbdb0a67 100644
--- a/features/roommembermoderation/impl/src/main/res/values-in/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-in/translations.xml
@@ -6,7 +6,7 @@
"Apakah Anda yakin ingin mencekal anggota ini?"
"Mencekal %1$s"
"Hapus"
- "Pengguna dapat bergabung dalam ruangan ini lagi jika diundang."
+ "Pengguna dapat bergabung ke ruangan ini lagi jika diundang."
"Apakah Anda yakin ingin menghapus anggota ini?"
"Tampilkan profil"
"Keluarkan dari ruangan"
diff --git a/features/roommembermoderation/impl/src/main/res/values-it/translations.xml b/features/roommembermoderation/impl/src/main/res/values-it/translations.xml
index 41b8baa473..7791d04546 100644
--- a/features/roommembermoderation/impl/src/main/res/values-it/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-it/translations.xml
@@ -13,8 +13,8 @@
"Rimuovere e vietare l\'accesso in futuro?"
"Rimozione di %1$s…"
"Riammetti nella stanza"
- "Togli ban"
+ "Riammetti"
"Potranno unirsi di nuovo alla stanza se invitati"
"Sei sicuro di voler sbloccare questo membro?"
- "Rimuovendo il ban di %1$s"
+ "Riammissione di %1$s"
diff --git a/features/roommembermoderation/impl/src/main/res/values-ka/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ka/translations.xml
index a2eb6d4bad..adfdde605c 100644
--- a/features/roommembermoderation/impl/src/main/res/values-ka/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-ka/translations.xml
@@ -5,8 +5,11 @@
"მოწვევის შემთხვევაში ამ ოთახში კვლავ გაწევრიანებას ვერ შეძლებენ."
"დარწმუნებული ხართ, რომ ამ წევრის დაბლოკვა გსურთ?"
"%1$s-ს დაბლოკვა"
+ "მოწვევის შემთხვევაში განბლოკილი მომხმარებელი ისევ შეძლებს ოთახს შეუერთდეს."
"პროფილის ნახვა"
"ოთახიდან გაგდება"
"გსურთ წევრის გაგდება და მომავალში გაწევრიანების აკრძალვა?"
"%1$s-ს გაგდება…"
+ "განბლოკვა"
+ "%1$s-ს განბლოკვა"
diff --git a/features/roommembermoderation/impl/src/main/res/values-ko/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ko/translations.xml
index 4831c177cc..0675c39f2f 100644
--- a/features/roommembermoderation/impl/src/main/res/values-ko/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-ko/translations.xml
@@ -6,14 +6,14 @@
"정말로 이 회원을 차단하시겠습니까?"
"차단 %1$s"
"제거"
- "초대되면 이 방에 다시 참여할 수 있습니다."
+ "초대받으면 이 방에 다시 들어올 수 있습니다."
"이 회원을 정말로 제거하시겠습니까?"
"프로필 보기"
"방에서 제거"
"회원을 삭제하고 앞으로 가입을 금지하시겠습니까?"
"%1$s 제거 중…"
"방에서 차단 해제"
- "차단 해제"
+ "금지 해제"
"초대되면 다시 방에 참여할 수 있습니다."
"이 회원을 정말로 차단해제 하시겠습니까?"
"차단 해제 %1$s"
diff --git a/features/roommembermoderation/impl/src/main/res/values-nb/translations.xml b/features/roommembermoderation/impl/src/main/res/values-nb/translations.xml
index 323526170b..4507fce49c 100644
--- a/features/roommembermoderation/impl/src/main/res/values-nb/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-nb/translations.xml
@@ -13,7 +13,7 @@
"Fjerne medlem og utestenge fra å bli med i fremtiden?"
"Fjerner %1$s…"
"Fjern utestengelsen fra rommet"
- "Opphev utestengelsen"
+ "Opphev utestengelse"
"De vil kunne bli med i rommet igjen hvis de blir invitert"
"Er du sikker på at du vil oppheve utestengelsen av dette medlemmet?"
"Oppheve utestengelsen av %1$s"
diff --git a/features/roommembermoderation/impl/src/main/res/values-nl/translations.xml b/features/roommembermoderation/impl/src/main/res/values-nl/translations.xml
index 64317b83bf..55aca0fbb9 100644
--- a/features/roommembermoderation/impl/src/main/res/values-nl/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-nl/translations.xml
@@ -5,8 +5,11 @@
"Ze kunnen niet meer toetreden tot deze kamer als ze worden uitgenodigd."
"Weet je zeker dat je dit lid wilt verbannen?"
"%1$s verbannen"
+ "Ze kunnen opnieuw tot de kamer toetreden als ze worden uitgenodigd."
"Profiel bekijken"
"Verwijderen uit kamer"
"Lid verwijderen en toekomstige deelname verbieden?"
"%1$s wordt verwijderd…"
+ "Ontbannen"
+ "%1$s ontbannen"
diff --git a/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml b/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml
index b94e3f8d8b..20246af787 100644
--- a/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml
@@ -6,7 +6,7 @@
"Czy na pewno chcesz zbanować tego członka?"
"Banowanie %1$s"
"Usuń"
- "Będą mogli ponownie dołączyć do pokoju, jeśli zostaną zaproszeni."
+ "Będą mogli ponownie dołączyć do tego pokoju, jeśli zostaną zaproszeni."
"Czy na pewno chcesz usunąć tego członka?"
"Wyświetl profil"
"Usuń z pokoju"
@@ -16,5 +16,5 @@
"Odbanuj"
"Mogą ponownie dołączyć do pokoju, po otrzymaniu zaproszenia"
"Czy na pewno chcesz odbanować tego członka?"
- "Odbanowuję %1$s"
+ "Odbanowanie %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 803eda1f80..4cbc0ba081 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
@@ -6,7 +6,7 @@
"Tem certeza de que quer banir este membro?"
"Banindo %1$s"
"Remover"
- "Essa pessoa poderá entrar na sala novamente se for convidada."
+ "Esta pessoa poderá entrar nesta 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 42ceede746..3989c4de7b 100644
--- a/features/roommembermoderation/impl/src/main/res/values-pt/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-pt/translations.xml
@@ -6,15 +6,15 @@
"Tens a certeza que queres banir este participante?"
"A banir %1$s"
"Remover"
- "Poderão entrar na sala novamente se convidados."
+ "Poderão juntar-se novamente a esta sala se forem convidados."
"Tens certeza que queres remover este membro?"
"Ver perfil"
"Remover da sala"
"Remover participante e proibir que entre no futuro?"
"A remover %1$s…"
"Desbanir da sala"
- "Desbanir"
+ "Anular banimento"
"Eles poderão entrar novamente na sala se forem convidados"
"Tens certeza que queres desbanir este membro?"
- "Desbanindo %1$s"
+ "A anular banimento de %1$s"
diff --git a/features/roommembermoderation/impl/src/main/res/values-ro/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ro/translations.xml
index fa05933234..4811b5263f 100644
--- a/features/roommembermoderation/impl/src/main/res/values-ro/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-ro/translations.xml
@@ -6,15 +6,15 @@
"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."
+ "Se vor putea alătura din nou acestei săli 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 îndepărtează %1$s"
"Revocati excluderea din camera"
- "Anulați excluderea"
+ "Anulare excludere"
"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"
+ "Se anulează interzicerea lui %1$s"
diff --git a/features/roommembermoderation/impl/src/main/res/values-sk/translations.xml b/features/roommembermoderation/impl/src/main/res/values-sk/translations.xml
index df5214c17c..ffdd634b0b 100644
--- a/features/roommembermoderation/impl/src/main/res/values-sk/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-sk/translations.xml
@@ -16,5 +16,5 @@
"Zrušiť zákaz"
"V prípade pozvania by sa mohli opäť pripojiť k miestnosti"
"Naozaj chcete zrušiť zablokovanie tohto člena?"
- "Zrušenie zákazu pre %1$s"
+ "Zrušenie zákazu %1$s"
diff --git a/features/roommembermoderation/impl/src/main/res/values-tr/translations.xml b/features/roommembermoderation/impl/src/main/res/values-tr/translations.xml
index 73f17edd02..a63e4eb55d 100644
--- a/features/roommembermoderation/impl/src/main/res/values-tr/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-tr/translations.xml
@@ -5,8 +5,11 @@
"Davet edilseler bile bu odaya tekrar katılamazlar."
"Bu üyeyi yasaklamak istediğinize emin misiniz?"
"Yasaklanıyor %1$s"
+ "Davet edildikleri takdirde bu odaya tekrar katılabileceklerdir."
"Profili görüntüle"
"Odadan çıkar"
"Üyeyi çıkarın ve gelecekte katılmasını yasaklayın?"
"Kaldırılıyor %1$s…"
+ "Yasağı Kaldır"
+ "Yasak kaldırılıyor %1$s"
diff --git a/features/roommembermoderation/impl/src/main/res/values-ur/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ur/translations.xml
index 333c86f056..2ea1262c4e 100644
--- a/features/roommembermoderation/impl/src/main/res/values-ur/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-ur/translations.xml
@@ -5,8 +5,11 @@
"اگر وہ مدعو کیا گیا تو وہ دوبارہ اس کمرے میں شامل نہیں ہوسکیں گے۔"
"کیا آپ کو یقین ہے کہ آپ اس رکن کو محظور کرنا چاہتے ہیں؟"
"%1$s کو محظور کر رہا ہے"
+ "اگر وہ مدعو کیا جائیں تو وہ دوبارہ اس کمرے میں شامل ہوسکیں گے۔"
"نمایہ ملاحظہ کریں"
"کمرے سے ہٹائیں"
"رکن کو ہٹائیں اور مستقبل میں شمولیت پر پابندی لگائیں؟"
"%1$s کو ہٹا رہا ہے…"
+ "غیر محظور کریں"
+ "%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
index 2b08fe58a0..d7b09f0e8d 100644
--- a/features/roommembermoderation/impl/src/main/res/values-uz/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-uz/translations.xml
@@ -5,8 +5,11 @@
"Taklif qilingan taqdirda ham, ular bu xonaga boshqa qo‘shila olmaydilar."
"Haqiqatan ham bu aʼzoni taqiqlamoqchimisiz?"
"Taqiqlash %1$s"
+ "Agar taklif qilinsa, ular bu xonaga qayta qo‘shilishlari mumkin."
"Profilni koʻrish"
"Xonadan olib tashlash"
"Aʻzo oʻchirilsinmi va kelgusida qoʻshilish taqiqlansinmi?"
"Oʻchirish %1$s …"
+ "Taqiqni bekor qilish"
+ "Taqiqni bekor qilish %1$s"
diff --git a/features/roommembermoderation/impl/src/main/res/values-zh-rTW/translations.xml b/features/roommembermoderation/impl/src/main/res/values-zh-rTW/translations.xml
index 1f2e372b8c..de8a9712ee 100644
--- a/features/roommembermoderation/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-zh-rTW/translations.xml
@@ -6,15 +6,15 @@
"您確定要將此成員加入黑名單?"
"正在將 %1$s 加入黑名單"
"移除"
- "若收到邀請,他們可以再次加入此聊天室。"
+ "如果收到邀請,他們能再次加入聊天室。"
"您真的想要移除此成員嗎?"
"查看個人檔案"
"踢出聊天室"
"移除成員並禁止未來再度加入?"
"正在踢出 %1$s…"
"從聊天室解除封鎖"
- "解除封鎖"
+ "解除黑名單"
"若受到邀請,他們仍可再次加入聊天室"
"您確定您想要取消封鎖此成員嗎?"
- "解除封鎖 %1$s"
+ "正在解除黑名單 %1$s"
diff --git a/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml
index 54a0978da1..a27d2ded05 100644
--- a/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml
@@ -13,7 +13,7 @@
"删除成员并禁止重新加入?"
"正在移除 %1$s……"
"从房间取消解封"
- "解除封禁"
+ "取消封禁"
"如果再次收到邀请,他们可以重新加入该聊天室"
"确定要解除该成员的封禁吗?"
"解除封禁 %1$s"
diff --git a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt
index 3b647c6478..40e09b3620 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
@@ -30,7 +30,7 @@ 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.collections.immutable.toImmutableList
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@@ -360,7 +360,7 @@ class RoomMemberModerationPresenterTest {
updateMembersResult = { Result.success(Unit) }
),
).apply {
- val roomMembers = listOfNotNull(targetRoomMember).toPersistentList()
+ val roomMembers = listOfNotNull(targetRoomMember).toImmutableList()
givenRoomMembersState(state = RoomMembersState.Ready(roomMembers))
}
}
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 b0cfafd915..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
@@ -18,7 +18,7 @@ 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.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -34,7 +34,7 @@ import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class SecureBackupFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 023fcd8f17..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
@@ -13,12 +13,12 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class SecureBackupDisableNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 14b2109506..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
@@ -14,12 +14,12 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class SecureBackupEnterRecoveryKeyNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 fe6a86a357..4ffaa9c344 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
@@ -10,7 +10,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
+import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
@@ -24,7 +24,7 @@ import kotlinx.coroutines.launch
@Inject
class ResetIdentityFlowManager(
- private val matrixClient: MatrixClient,
+ private val encryptionService: EncryptionService,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val sessionVerificationService: SessionVerificationService,
) {
@@ -46,7 +46,7 @@ class ResetIdentityFlowManager(
resetHandleFlow.value = AsyncData.Loading()
sessionCoroutineScope.launch {
- matrixClient.encryptionService().startIdentityReset()
+ encryptionService.startIdentityReset()
.onSuccess { handle ->
resetHandleFlow.value = AsyncData.Success(handle)
}
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 8abe8e5ffb..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
@@ -24,7 +24,7 @@ import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -47,7 +47,7 @@ import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class ResetIdentityFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 dd4218d344..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
@@ -13,7 +13,7 @@ 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.Inject
+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
@@ -22,7 +22,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class ResetIdentityPasswordNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 b6a0883eb7..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
@@ -13,12 +13,12 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class ResetIdentityRootNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 a7b22a64e3..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
@@ -16,13 +16,13 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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)
-@Inject
+@AssistedInject
class SecureBackupRootNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 322546bc56..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
@@ -13,7 +13,7 @@ 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.Inject
+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
@@ -23,7 +23,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class SecureBackupSetupNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 35dd8356da..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
@@ -20,7 +20,7 @@ import com.freeletics.flowredux.compose.StateAndDispatch
import com.freeletics.flowredux.compose.rememberStateAndDispatch
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import timber.log.Timber
-@Inject
+@AssistedInject
class SecureBackupSetupPresenter(
@Assisted private val isChangeRecoveryKeyUserStory: Boolean,
private val stateMachine: SecureBackupSetupStateMachine,
diff --git a/features/securebackup/impl/src/main/res/values-eo/translations.xml b/features/securebackup/impl/src/main/res/values-eo/translations.xml
index e3ea61ec1c..4b8e1833a3 100644
--- a/features/securebackup/impl/src/main/res/values-eo/translations.xml
+++ b/features/securebackup/impl/src/main/res/values-eo/translations.xml
@@ -23,9 +23,11 @@
"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"
diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt
index a26d6fabef..702b10a1bf 100644
--- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt
+++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt
@@ -12,7 +12,6 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
-import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.encryption.FakeIdentityPasswordResetHandle
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
@@ -130,10 +129,9 @@ class ResetIdentityFlowManagerTest {
private fun TestScope.createFlowManager(
encryptionService: FakeEncryptionService = FakeEncryptionService(),
- client: FakeMatrixClient = FakeMatrixClient(encryptionService = encryptionService),
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
) = ResetIdentityFlowManager(
- matrixClient = client,
+ encryptionService = encryptionService,
sessionCoroutineScope = this,
sessionVerificationService = sessionVerificationService,
)
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 08b6448ac4..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
@@ -19,7 +19,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -31,7 +31,7 @@ import io.element.android.libraries.roomselect.api.RoomSelectMode
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class ShareNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 056d8b912e..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
@@ -13,7 +13,7 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.coroutines.cancellation.CancellationException
-@Inject
+@AssistedInject
class SharePresenter(
@Assisted private val intent: Intent,
@SessionCoroutineScope
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 b98a38f29d..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
@@ -14,14 +14,14 @@ 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.Inject
+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.matrix.api.core.SessionId
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class SignedOutNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
@@ -32,7 +32,7 @@ class SignedOutNode(
) : 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 9810892ff3..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,44 +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 dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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
-@Inject
+@AssistedInject
class SignedOutPresenter(
- // Cannot inject SessionId
- @Assisted private val sessionId: String,
+ @Assisted private val sessionId: SessionId,
private val sessionStore: SessionStore,
private val buildMeta: BuildMeta,
) : Presenter {
@AssistedFactory
fun interface Factory {
- fun create(sessionId: String): SignedOutPresenter
+ 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/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
index 860ad88d8a..6366ba5ea1 100644
--- 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
@@ -28,7 +28,7 @@ class DefaultSignedOutEntryPointTest {
buildContext = buildContext,
plugins = plugins,
presenterFactory = { sessionId ->
- assertThat(sessionId).isEqualTo(A_SESSION_ID.value)
+ assertThat(sessionId).isEqualTo(A_SESSION_ID)
createSignedOutPresenter()
}
)
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 de5634f269..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
@@ -70,7 +70,7 @@ internal fun createSignedOutPresenter(
sessionStore: SessionStore = InMemorySessionStore(),
): SignedOutPresenter {
return SignedOutPresenter(
- sessionId = sessionId.value,
+ sessionId = sessionId,
sessionStore = sessionStore,
buildMeta = aBuildMeta(applicationName = AN_APPLICATION_NAME),
)
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
index 5f7be8dba0..bdea93f3ef 100644
--- 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
@@ -31,6 +31,6 @@ interface SpaceEntryPoint : FeatureEntryPoint {
) : NodeInputs
interface Callback : Plugin {
- fun onOpenRoom(roomId: RoomId)
+ fun onOpenRoom(roomId: RoomId, viaParameters: List)
}
}
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
index 1ef8275b27..8591978417 100644
--- 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
@@ -33,7 +33,7 @@ class DefaultSpaceEntryPoint : SpaceEntryPoint {
}
override fun build(): Node {
- return parentNode.createNode(buildContext, plugins = plugins.toList())
+ 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..78472c7b31
--- /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.core.RoomId
+import io.element.android.libraries.matrix.api.spaces.SpaceService
+import kotlinx.parcelize.Parcelize
+
+@ContributesNode(SessionScope::class)
+@AssistedInject
+class SpaceFlowNode(
+ @Assisted val buildContext: BuildContext,
+ @Assisted plugins: List,
+ spaceService: SpaceService,
+ 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 = 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/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpacePresenter.kt
deleted file mode 100644
index 1955ab652e..0000000000
--- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpacePresenter.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright 2025 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.space.impl
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
-import io.element.android.features.invite.api.SeenInvitesStore
-import io.element.android.features.space.api.SpaceEntryPoint
-import io.element.android.libraries.architecture.Presenter
-import io.element.android.libraries.core.coroutine.mapState
-import io.element.android.libraries.matrix.api.MatrixClient
-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.toPersistentSet
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-import java.util.Optional
-import kotlin.jvm.optionals.getOrNull
-
-@Inject
-class SpacePresenter(
- @Assisted private val inputs: SpaceEntryPoint.Inputs,
- private val client: MatrixClient,
- private val seenInvitesStore: SeenInvitesStore,
-) : Presenter {
- @AssistedFactory
- fun interface Factory {
- fun create(inputs: SpaceEntryPoint.Inputs): SpacePresenter
- }
-
- private val spaceRoomList = client.spaceService.spaceRoomList(inputs.roomId)
-
- @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 coroutineScope = 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 remember { spaceRoomList.currentSpaceFlow() }.collectAsState(Optional.empty())
-
- fun handleEvents(event: SpaceEvents) {
- when (event) {
- SpaceEvents.LoadMore -> coroutineScope.paginate()
- }
- }
- return SpaceState(
- currentSpace = currentSpace.getOrNull(),
- children = children.toPersistentList(),
- seenSpaceInvites = seenSpaceInvites,
- hideInvitesAvatar = hideInvitesAvatar,
- hasMoreToLoad = hasMoreToLoad,
- eventSink = ::handleEvents,
- )
- }
-
- private fun CoroutineScope.paginate() = launch {
- spaceRoomList.paginate()
- }
-}
diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceState.kt
deleted file mode 100644
index ad822283ca..0000000000
--- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceState.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright 2025 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.space.impl
-
-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.ImmutableSet
-
-data class SpaceState(
- val currentSpace: SpaceRoom?,
- val children: ImmutableList,
- val seenSpaceInvites: ImmutableSet,
- val hideInvitesAvatar: Boolean,
- val hasMoreToLoad: Boolean,
- val eventSink: (SpaceEvents) -> Unit
-)
diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceStateProvider.kt
deleted file mode 100644
index cf2fcf92b5..0000000000
--- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceStateProvider.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2025 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.space.impl
-
-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.toImmutableList
-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()
- )
- // 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(),
- hideInvitesAvatar: Boolean = false,
- hasMoreToLoad: Boolean = false,
-) = SpaceState(
- currentSpace = parentSpace,
- children = children.toImmutableList(),
- seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
- hideInvitesAvatar = hideInvitesAvatar,
- hasMoreToLoad = hasMoreToLoad,
- eventSink = {}
-)
-
-private 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/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceView.kt
deleted file mode 100644
index 53aa61a0c7..0000000000
--- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceView.kt
+++ /dev/null
@@ -1,205 +0,0 @@
-/*
- * Copyright 2025 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.space.impl
-
-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.rememberUpdatedState
-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.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.Scaffold
-import io.element.android.libraries.designsystem.theme.components.Text
-import io.element.android.libraries.designsystem.theme.components.TopAppBar
-import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.room.CurrentUserMembership
-import io.element.android.libraries.matrix.api.spaces.SpaceRoom
-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
-
-@Composable
-fun SpaceView(
- state: SpaceState,
- onBackClick: () -> Unit,
- onRoomClick: (roomId: RoomId) -> Unit,
- modifier: Modifier = Modifier,
-) {
- Scaffold(
- modifier = modifier,
- topBar = {
- SpaceViewTopBar(currentSpace = state.currentSpace, onBackClick = onBackClick)
- },
- content = { padding ->
- Box(
- modifier = Modifier.padding(padding)
- ) {
- SpaceViewContent(
- state = state,
- onRoomClick = onRoomClick
- )
- }
- },
- )
-}
-
-@Composable
-private fun SpaceViewContent(
- state: SpaceState,
- onRoomClick: (roomId: RoomId) -> 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
- SpaceRoomItemView(
- spaceRoom = spaceRoom,
- showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
- hideAvatars = isInvitation && state.hideInvitesAvatar,
- onClick = {
- onRoomClick(spaceRoom.roomId)
- },
- onLongClick = {
- // TODO
- }
- )
- }
- }
- 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,
- modifier: Modifier = Modifier,
-) {
- TopAppBar(
- modifier = modifier,
- navigationIcon = {
- BackButton(onClick = onBackClick)
- },
- title = {
- if (currentSpace != null) {
- SpaceAvatarAndNameRow(
- name = currentSpace.name,
- avatarData = currentSpace.getAvatarData(AvatarSize.TimelineRoom),
- )
- }
- },
- actions = {
- },
- )
-}
-
-@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
- )
- }
-}
-
-@PreviewsDayNight
-@Composable
-internal fun SpaceViewPreview(
- @PreviewParameter(SpaceStateProvider::class) state: SpaceState
-) = ElementPreview {
- SpaceView(
- state = state,
- onRoomClick = {},
- onBackClick = {},
- )
-}
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..558ae8454d
--- /dev/null
+++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceEvents.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.space.impl.leave
+
+import io.element.android.libraries.matrix.api.core.RoomId
+
+sealed interface LeaveSpaceEvents {
+ data object Retry : 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..c60bddea1d
--- /dev/null
+++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.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.space.impl.leave
+
+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 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.SpaceFlowScope
+import io.element.android.libraries.architecture.inputs
+import io.element.android.libraries.matrix.api.MatrixClient
+
+@ContributesNode(SpaceFlowScope::class)
+@AssistedInject
+class LeaveSpaceNode(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ matrixClient: MatrixClient,
+ presenterFactory: LeaveSpacePresenter.Factory,
+) : Node(buildContext, plugins = plugins) {
+ private val inputs: SpaceEntryPoint.Inputs = inputs()
+ private val leaveSpaceHandle = matrixClient.spaceService.getLeaveSpaceHandle(inputs.roomId)
+ private val presenter: LeaveSpacePresenter = presenterFactory.create(leaveSpaceHandle)
+
+ override fun onBuilt() {
+ super.onBuilt()
+ lifecycle.subscribe(
+ onDestroy = {
+ leaveSpaceHandle.close()
+ }
+ )
+ }
+
+ @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..19e593bf7c
--- /dev/null
+++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: 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.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import dev.zacsweers.metro.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
+import io.element.android.libraries.architecture.map
+import io.element.android.libraries.architecture.runUpdatingState
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
+import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentSetOf
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@AssistedInject
+class LeaveSpacePresenter(
+ @Assisted private val leaveSpaceHandle: LeaveSpaceHandle,
+) : Presenter {
+ @AssistedFactory
+ fun interface Factory {
+ fun create(leaveSpaceHandle: LeaveSpaceHandle): LeaveSpacePresenter
+ }
+
+ data class LeaveSpaceRooms(
+ val current: LeaveSpaceRoom?,
+ val others: List,
+ )
+
+ @Composable
+ override fun present(): LeaveSpaceState {
+ val coroutineScope = rememberCoroutineScope()
+ var retryCount by remember { mutableIntStateOf(0) }
+ val leaveSpaceAction = remember {
+ mutableStateOf>(AsyncAction.Uninitialized)
+ }
+ var selectedRoomIds by remember {
+ mutableStateOf>(setOf())
+ }
+ var leaveSpaceRooms by remember {
+ mutableStateOf>(AsyncData.Loading())
+ }
+ LaunchedEffect(retryCount) {
+ val rooms = leaveSpaceHandle.rooms()
+ val (currentRoom, otherRooms) = rooms.getOrNull()
+ .orEmpty()
+ .partition { it.spaceRoom.roomId == leaveSpaceHandle.id }
+ // By default select all rooms that can be left
+ val otherRoomsExcludingDm = otherRooms.filter { it.spaceRoom.isDirect != true }
+ selectedRoomIds = otherRoomsExcludingDm
+ .filter { it.isLastAdmin.not() }
+ .map { it.spaceRoom.roomId }
+ leaveSpaceRooms = rooms.fold(
+ onSuccess = {
+ AsyncData.Success(
+ LeaveSpaceRooms(
+ current = currentRoom.firstOrNull(),
+ others = otherRoomsExcludingDm.toImmutableList(),
+ )
+ )
+ },
+ onFailure = { AsyncData.Failure(it) }
+ )
+ }
+ var selectableSpaceRooms by remember {
+ mutableStateOf>>(AsyncData.Loading())
+ }
+ LaunchedEffect(selectedRoomIds, leaveSpaceRooms) {
+ selectableSpaceRooms = leaveSpaceRooms.map {
+ it?.others.orEmpty().map { room ->
+ SelectableSpaceRoom(
+ spaceRoom = room.spaceRoom,
+ isLastAdmin = room.isLastAdmin,
+ isSelected = selectedRoomIds.contains(room.spaceRoom.roomId),
+ )
+ }.toImmutableList()
+ }
+ }
+
+ fun handleEvents(event: LeaveSpaceEvents) {
+ when (event) {
+ LeaveSpaceEvents.Retry -> {
+ leaveSpaceRooms = AsyncData.Loading()
+ retryCount += 1
+ }
+ LeaveSpaceEvents.DeselectAllRooms -> {
+ selectedRoomIds = persistentSetOf()
+ }
+ LeaveSpaceEvents.SelectAllRooms -> {
+ selectedRoomIds = selectableSpaceRooms.dataOrNull()
+ .orEmpty()
+ .filter { it.isLastAdmin.not() }
+ .map { it.spaceRoom.roomId }
+ }
+ is LeaveSpaceEvents.ToggleRoomSelection -> {
+ selectedRoomIds = if (selectedRoomIds.contains(event.roomId)) {
+ selectedRoomIds - event.roomId
+ } else {
+ selectedRoomIds + event.roomId
+ }
+ }
+ LeaveSpaceEvents.LeaveSpace -> coroutineScope.leaveSpace(
+ leaveSpaceAction = leaveSpaceAction,
+ selectedRoomIds = selectedRoomIds,
+ )
+ LeaveSpaceEvents.CloseError -> {
+ leaveSpaceAction.value = AsyncAction.Uninitialized
+ }
+ }
+ }
+
+ return LeaveSpaceState(
+ spaceName = leaveSpaceRooms.dataOrNull()?.current?.spaceRoom?.displayName,
+ isLastAdmin = leaveSpaceRooms.dataOrNull()?.current?.isLastAdmin == true,
+ selectableSpaceRooms = selectableSpaceRooms,
+ leaveSpaceAction = leaveSpaceAction.value,
+ eventSink = ::handleEvents,
+ )
+ }
+
+ private fun CoroutineScope.leaveSpace(
+ leaveSpaceAction: MutableState>,
+ selectedRoomIds: Collection,
+ ) = launch {
+ runUpdatingState(leaveSpaceAction) {
+ leaveSpaceHandle.leave(selectedRoomIds.toList())
+ }
+ }
+}
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..0f2a0f93f6
--- /dev/null
+++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.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 isLastAdmin: Boolean,
+ 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 = isLastAdmin.not() && selectableRooms.isNotEmpty()
+
+ /**
+ * True if we should show the leave button.
+ */
+ val showLeaveButton = isLastAdmin.not() && selectableSpaceRooms is AsyncData.Success
+
+ /**
+ * 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..fec625ca13
--- /dev/null
+++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.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.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.toImmutableList
+
+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(
+ displayName = "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() }.toImmutableList()
+ ),
+ leaveSpaceAction = AsyncAction.Loading,
+ ),
+ aLeaveSpaceState(
+ selectableSpaceRooms = AsyncData.Success(
+ List(10) { aSelectableSpaceRoom() }.toImmutableList()
+ ),
+ leaveSpaceAction = AsyncAction.Failure(Exception("An error")),
+ ),
+ aLeaveSpaceState(
+ selectableSpaceRooms = AsyncData.Failure(Exception("An error")),
+ ),
+ aLeaveSpaceState(
+ isLastAdmin = true,
+ ),
+ )
+}
+
+fun aLeaveSpaceState(
+ spaceName: String? = "Space name",
+ isLastAdmin: Boolean = false,
+ selectableSpaceRooms: AsyncData> = AsyncData.Uninitialized,
+ leaveSpaceAction: AsyncAction = AsyncAction.Uninitialized,
+) = LeaveSpaceState(
+ spaceName = spaceName,
+ isLastAdmin = isLastAdmin,
+ 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..7432301f91
--- /dev/null
+++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt
@@ -0,0 +1,349 @@
+/*
+ * Copyright 2025 New 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.annotation.StringRes
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+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,
+ topBar = {
+ LeaveSpaceHeader(
+ state = state,
+ onBackClick = onCancel,
+ )
+ },
+ containerColor = ElementTheme.colors.bgCanvasDefault,
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .padding(padding)
+ .imePadding()
+ .consumeWindowInsets(padding)
+ .fillMaxSize()
+ ) {
+ LazyColumn(
+ modifier = Modifier
+ .weight(1f),
+ ) {
+ if (state.isLastAdmin.not()) {
+ 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 = {
+ state.eventSink(LeaveSpaceEvents.Retry)
+ },
+ )
+ }
+ is AsyncData.Loading,
+ AsyncData.Uninitialized -> item {
+ AsyncLoading()
+ }
+ }
+ }
+ }
+ LeaveSpaceButtons(
+ showLeaveButton = state.showLeaveButton,
+ selectedRoomsCount = state.selectedRoomsCount,
+ onLeaveSpace = {
+ state.eventSink(LeaveSpaceEvents.LeaveSpace)
+ },
+ onCancel = onCancel,
+ )
+ }
+ }
+
+ AsyncActionView(
+ async = state.leaveSpaceAction,
+ onSuccess = { /* Nothing to do, the screen will be dismissed automatically */ },
+ errorMessage = { stringResource(CommonStrings.error_unknown) },
+ 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(
+ if (state.isLastAdmin) R.string.screen_leave_space_title_last_admin else R.string.screen_leave_space_title,
+ state.spaceName ?: stringResource(CommonStrings.common_space)
+ ),
+ subTitle =
+ if (state.isLastAdmin) {
+ stringResource(R.string.screen_leave_space_subtitle_last_admin)
+ } else 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) {
+ QuickActionButton(CommonStrings.action_deselect_all) {
+ state.eventSink(LeaveSpaceEvents.DeselectAllRooms)
+ }
+ } else {
+ QuickActionButton(resId = CommonStrings.action_select_all) {
+ state.eventSink(LeaveSpaceEvents.SelectAllRooms)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun ColumnScope.QuickActionButton(
+ @StringRes resId: Int,
+ onClick: () -> Unit,
+) {
+ Text(
+ modifier = Modifier
+ .align(Alignment.End)
+ .padding(end = 8.dp)
+ .clickable(onClick = onClick)
+ .padding(8.dp),
+ text = stringResource(resId),
+ color = ElementTheme.colors.textActionPrimary,
+ style = ElementTheme.typography.fontBodyMdMedium,
+ )
+}
+
+@Composable
+private fun LeaveSpaceButtons(
+ showLeaveButton: Boolean,
+ selectedRoomsCount: Int,
+ onLeaveSpace: () -> Unit,
+ onCancel: () -> Unit,
+) {
+ ButtonColumnMolecule(
+ modifier = Modifier.padding(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,
+ )
+ }
+ // TODO For least admin space, add a button to open the settings.
+ // See https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4622-59600
+ 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.displayName,
+ 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 membersCount = pluralStringResource(
+ CommonPlurals.common_member_count,
+ room.numJoinedMembers,
+ room.numJoinedMembers
+ )
+ val subTitle = if (selectableSpaceRoom.isLastAdmin) {
+ stringResource(R.string.screen_leave_space_last_admin_info, membersCount)
+ } else {
+ membersCount
+ }
+ 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..8c8eb8dbb7
--- /dev/null
+++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.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
+
+ data class ShowTopicViewer(val topic: String) : SpaceEvents
+ data object HideTopicViewer : 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..58f5c8f5fb
--- /dev/null
+++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.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 androidx.compose.runtime.setValue
+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.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.persistentSetOf
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.collections.immutable.toImmutableMap
+import kotlinx.collections.immutable.toImmutableSet
+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 {
+ private var children by mutableStateOf>(persistentListOf())
+
+ @Composable
+ override fun present(): SpaceState {
+ LaunchedEffect(Unit) {
+ paginate()
+ spaceRoomList.spaceRoomsFlow.collect { children = it.toImmutableList() }
+ }
+
+ val hideInvitesAvatar by client.rememberHideInvitesAvatar()
+ val seenSpaceInvites by remember {
+ seenInvitesStore.seenRoomIds().map { it.toImmutableSet() }
+ }.collectAsState(persistentSetOf())
+
+ val localCoroutineScope = rememberCoroutineScope()
+
+ 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>()) }
+
+ var topicViewerState: TopicViewerState by remember { mutableStateOf(TopicViewerState.Hidden) }
+
+ 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)
+ )
+ }
+ SpaceEvents.HideTopicViewer -> topicViewerState = TopicViewerState.Hidden
+ is SpaceEvents.ShowTopicViewer -> topicViewerState = TopicViewerState.Shown(event.topic)
+ }
+ }
+ return SpaceState(
+ currentSpace = currentSpace.getOrNull(),
+ children = children,
+ seenSpaceInvites = seenSpaceInvites,
+ hideInvitesAvatar = hideInvitesAvatar,
+ hasMoreToLoad = hasMoreToLoad,
+ joinActions = joinActions.toImmutableMap(),
+ acceptDeclineInviteState = acceptDeclineInviteState,
+ topicViewerState = topicViewerState,
+ 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..2499e4c046
--- /dev/null
+++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.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.root
+
+import androidx.compose.runtime.Immutable
+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 topicViewerState: TopicViewerState,
+ val eventSink: (SpaceEvents) -> Unit
+) {
+ fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading
+ val hasAnyFailure: Boolean = joinActions.values.any {
+ it is AsyncAction.Failure
+ }
+}
+
+@Immutable
+sealed interface TopicViewerState {
+ data object Hidden : TopicViewerState
+ data class Shown(val topic: String) : TopicViewerState
+}
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..98a7363abb
--- /dev/null
+++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.space.impl.root
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
+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.room.join.JoinRule
+import io.element.android.libraries.matrix.api.spaces.SpaceRoom
+import io.element.android.libraries.previewutils.room.aSpaceRoom
+import kotlinx.collections.immutable.persistentListOf
+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 = aParentSpace(joinRule = JoinRule.Public)),
+ aSpaceState(parentSpace = aParentSpace(joinRule = JoinRule.Restricted(persistentListOf()))),
+ aSpaceState(children = aListOfSpaceRooms()),
+ aSpaceState(
+ parentSpace = aParentSpace(),
+ children = aListOfSpaceRooms(),
+ joiningRooms = setOf(RoomId("!spaceId0:example.com")),
+ hasMoreToLoad = false
+ ),
+ aSpaceState(
+ topicViewerState = TopicViewerState.Shown(topic = "Space description goes here." + LoremIpsum(20).values.first()),
+ ),
+ // Add other states here
+ )
+}
+
+fun aSpaceState(
+ parentSpace: SpaceRoom? = aParentSpace(),
+ children: List = emptyList(),
+ seenSpaceInvites: Set = emptySet(),
+ joiningRooms: Set = emptySet(),
+ joinActions: Map> = joiningRooms.associateWith { AsyncAction.Loading },
+ hideInvitesAvatar: Boolean = false,
+ hasMoreToLoad: Boolean = true,
+ acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
+ topicViewerState: TopicViewerState = TopicViewerState.Hidden,
+ eventSink: (SpaceEvents) -> Unit = { },
+) = SpaceState(
+ currentSpace = parentSpace,
+ children = children.toImmutableList(),
+ seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
+ hideInvitesAvatar = hideInvitesAvatar,
+ hasMoreToLoad = hasMoreToLoad,
+ joinActions = joinActions.toImmutableMap(),
+ acceptDeclineInviteState = acceptDeclineInviteState,
+ topicViewerState = topicViewerState,
+ eventSink = eventSink,
+)
+
+private fun aParentSpace(
+ joinRule: JoinRule? = null,
+): SpaceRoom {
+ return aSpaceRoom(
+ numJoinedMembers = 5,
+ childrenCount = 10,
+ worldReadable = true,
+ joinRule = joinRule,
+ roomId = RoomId("!spaceId0:example.com"),
+ topic = "Space description goes here. " + LoremIpsum(20).values.first(),
+ )
+}
+
+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..46c6a10e57
--- /dev/null
+++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt
@@ -0,0 +1,387 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: 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.interaction.MutableInteractionSource
+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.ClickableLinkText
+import io.element.android.libraries.designsystem.components.SimpleModalBottomSheet
+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
+
+@OptIn(ExperimentalMaterial3Api::class)
+@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,
+ onTopicClick = { topic ->
+ state.eventSink(SpaceEvents.ShowTopicViewer(topic))
+ }
+ )
+ JoinRoomFailureEffect(
+ hasAnyFailure = state.hasAnyFailure,
+ eventSink = state.eventSink
+ )
+ acceptDeclineInviteView()
+ }
+ },
+ )
+ if (state.topicViewerState is TopicViewerState.Shown) {
+ TopicViewerBottomSheet(
+ topicViewerState = state.topicViewerState,
+ onDismiss = {
+ state.eventSink(SpaceEvents.HideTopicViewer)
+ }
+ )
+ }
+}
+
+@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 TopicViewerBottomSheet(
+ topicViewerState: TopicViewerState.Shown,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ SimpleModalBottomSheet(
+ title = stringResource(CommonStrings.common_description),
+ onDismiss = onDismiss,
+ modifier = modifier
+ ) {
+ ClickableLinkText(
+ text = topicViewerState.topic,
+ interactionSource = remember { MutableInteractionSource() },
+ style = ElementTheme.typography.fontBodyMdRegular,
+ color = ElementTheme.colors.textSecondary,
+ )
+ }
+}
+
+@Composable
+private fun SpaceViewContent(
+ state: SpaceState,
+ onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
+ onTopicClick: (String) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ LazyColumn(modifier.fillMaxSize()) {
+ val currentSpace = state.currentSpace
+ if (currentSpace != null) {
+ item {
+ SpaceHeaderView(
+ avatarData = currentSpace.getAvatarData(AvatarSize.SpaceHeader),
+ name = currentSpace.displayName,
+ topic = currentSpace.topic,
+ topicMaxLines = 2,
+ visibility = currentSpace.visibility,
+ heroes = currentSpace.heroes.toImmutableList(),
+ numberOfMembers = currentSpace.numJoinedMembers,
+ onTopicClick = onTopicClick
+ )
+ }
+ }
+ 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,
+ onLeaveSpaceClick: () -> Unit,
+ onShareSpace: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ TopAppBar(
+ modifier = modifier,
+ navigationIcon = {
+ BackButton(onClick = onBackClick)
+ },
+ title = {
+ if (currentSpace != null) {
+ SpaceAvatarAndNameRow(
+ name = currentSpace.displayName,
+ 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,
+ )
+ }
+ )
+ DropdownMenuItem(
+ onClick = {
+ showMenu = false
+ onLeaveSpaceClick()
+ },
+ text = {
+ Text(
+ text = stringResource(id = CommonStrings.action_leave),
+ color = ElementTheme.colors.textCriticalPrimary,
+ )
+ },
+ leadingIcon = {
+ Icon(
+ imageVector = CompoundIcons.Leave(),
+ tint = ElementTheme.colors.iconCriticalPrimary,
+ 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..0c645f0650
--- /dev/null
+++ b/features/space/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "%1$s (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..d9d6c02ebe
--- /dev/null
+++ b/features/space/impl/src/main/res/values-cy/translations.xml
@@ -0,0 +1,14 @@
+
+
+ "%1$s (Gweinyddwr)"
+
+ - "Gadael %1$d ystafelloedd a gofodau"
+ - "Gadael %1$d ystafell a gofod"
+ - "Gadael %1$d ystafell a gofod"
+ - "Gadael %1$d ystafell a gofod"
+ - "Gadael %1$d ystafell a gofod"
+ - "Gadael %1$d ystafell a gofod"
+
+ "Dewiswch yr ystafelloedd yr hoffech chi eu gadael nad chi yw\'r unig weinyddwr ar eu cyfer:"
+ "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..7953f32e99
--- /dev/null
+++ b/features/space/impl/src/main/res/values-da/translations.xml
@@ -0,0 +1,13 @@
+
+
+ "%1$s (Admin)"
+
+ - "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:"
+ "Du skal tildele en anden administrator til denne klynge, før du kan forlade den."
+ "Du vil ikke blive fjernet fra følgende rum, fordi du er den eneste administrator:"
+ "Forlad %1$s?"
+ "Du er den eneste administrator for %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..6fd5d7c76b
--- /dev/null
+++ b/features/space/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,13 @@
+
+
+ "%1$s (Admin)"
+
+ - "%1$d Chat und Space verlassen"
+ - "%1$d Chats und Space verlassen"
+
+ "Dadurch wirst du auch aus allen Chats in diesem Space entfernt."
+ "Du musst einen anderen Admin für diesen Space zuweisen, bevor du ihn verlassen kannst."
+ "Du wirst aus den folgenden Chats nicht entfernt, weil du der einzige Admin bist:"
+ "%1$s verlassen?"
+ "Du bist der einzige Administrator für %1$s"
+
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..3ab5d65284
--- /dev/null
+++ b/features/space/impl/src/main/res/values-et/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "%1$s (Peakasutaja)"
+
+ - "Lahku %1$d-st jututoast ja kogukonnast"
+ - "Lahku %1$d-st jututoast ja kogukonnast"
+
+ "Sellega eemaldad end ka kõikidest antud kogukonna jututubadest."
+ "Sind ei saa järgnevatest jututubadest eemaldada, kuna oled seal/neis ainus peakasutaja:"
+ "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..501ff2f97d
--- /dev/null
+++ b/features/space/impl/src/main/res/values-fi/translations.xml
@@ -0,0 +1,12 @@
+
+
+
+ - "Poistu %1$d huoneesta ja tilasta"
+ - "Poistu %1$d huoneesta ja tilasta"
+
+ "Tämä poistaa sinut myös kaikista tämän tilan huoneista."
+ "Sinun on valittava tälle tilalle toinen ylläpitäjä ennen kuin voit poistua."
+ "Sinua ei poisteta seuraavista huoneista, koska olet ainoa ylläpitäjä:"
+ "Haluatko poistua tilasta %1$s?"
+ "Olet ainoa ylläpitäjä tilassa %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..cf37795848
--- /dev/null
+++ b/features/space/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,13 @@
+
+
+ "%1$s (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:"
+ "Vous devez désigner un autre administrateur pour cet espace avant de pouvoir partir."
+ "Vous ne quitterez pas le ou les salons suivants car vous y êtes le seul administrateur:"
+ "Quitter %1$s?"
+ "Vous êtes le seul administrateur de %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..79a8733df2
--- /dev/null
+++ b/features/space/impl/src/main/res/values-hu/translations.xml
@@ -0,0 +1,10 @@
+
+
+ "%1$s (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-nb/translations.xml b/features/space/impl/src/main/res/values-nb/translations.xml
new file mode 100644
index 0000000000..aafe1d756f
--- /dev/null
+++ b/features/space/impl/src/main/res/values-nb/translations.xml
@@ -0,0 +1,9 @@
+
+
+ "%1$s (Admin)"
+ "Velg rommene du vil forlate, som du ikke er den eneste administratoren for:"
+ "Du må tildele en annen administrator for dette området før du kan forlate det."
+ "Du vil ikke bli fjernet fra følgende rom fordi du er den eneste administratoren:"
+ "Forlat %1$s?"
+ "Du er den eneste administratoren for %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-ro/translations.xml b/features/space/impl/src/main/res/values-ro/translations.xml
new file mode 100644
index 0000000000..6ff2a5dfa8
--- /dev/null
+++ b/features/space/impl/src/main/res/values-ro/translations.xml
@@ -0,0 +1,14 @@
+
+
+ "%1$s (Admin)"
+
+ - "Părăsiți %1$d cameră și spațiul"
+ - "Părăsiți %1$d camere și spațiul"
+ - "Părăsiți %1$d camere și spațiul"
+
+ "Selectați camerele pe care doriți să le părăsiți și în care nu sunteți singurul administrator:"
+ "Trebuie să desemnați un alt administrator pentru acest spațiu înainte de a-l părăsi."
+ "Nu veți părăsi următoarele camere deoarece sunteți singurul administrator:"
+ "Părăsiți %1$s?"
+ "Sunteți singurul administrator pentru %1$s"
+
diff --git a/features/space/impl/src/main/res/values-ru/translations.xml b/features/space/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..0df1b9224a
--- /dev/null
+++ b/features/space/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,14 @@
+
+
+ "%1$s (Администратор)"
+
+ - "Покинуть %1$d комнату и пространство"
+ - "Покинуть %1$d комнат и пространство"
+ - "Покинуть %1$d комнат и пространство"
+
+ "Выберите комнаты, которые вы хотите покинуть и в которых вы не являетесь единственным администратором:"
+ "Прежде чем покинуть это пространство, вам необходимо назначить другого администратора."
+ "Вы не будете удалены из следующих комнат, поскольку вы являетесь единственным администратором:"
+ "Выйти из %1$s?"
+ "Вы единственный администратор для %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-zh/translations.xml b/features/space/impl/src/main/res/values-zh/translations.xml
new file mode 100644
index 0000000000..4e95141bad
--- /dev/null
+++ b/features/space/impl/src/main/res/values-zh/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "%1$s (管理员)"
+
+ - "离开 %1$d 个房间和空间"
+
+ "选择您想要离开且您不是其唯一管理员的房间:"
+ "您需要为该空间指定另一位管理员才能离开。"
+ "您不会从以下房间中被移除,因为您是唯一的管理员:"
+ "离开%1$s?"
+ "您是 %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..c6ced29d41
--- /dev/null
+++ b/features/space/impl/src/main/res/values/localazy.xml
@@ -0,0 +1,13 @@
+
+
+ "%1$s (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 need to assign another admin for this space before you can leave."
+ "You will not be removed from the following room(s) because you\'re the only administrator:"
+ "Leave %1$s?"
+ "You are the only admin for %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
index 465fde3425..17823ba72b 100644
--- 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
@@ -9,12 +9,12 @@ 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.invite.test.InMemorySeenInvitesStore
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
@@ -26,38 +26,31 @@ 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 ->
- SpaceNode(
+ SpaceFlowNode(
buildContext = buildContext,
plugins = plugins,
- presenterFactory = { inputs ->
- assertThat(inputs).isEqualTo(nodeInputs)
- SpacePresenter(
- inputs = inputs,
- client = FakeMatrixClient(
- spaceService = FakeSpaceService(
- spaceRoomListResult = { FakeSpaceRoomList() },
- )
- ),
- seenInvitesStore = InMemorySeenInvitesStore(),
- )
- },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { _: RoomId -> FakeSpaceRoomList(A_ROOM_ID) }
+ ),
+ graphFactory = FakeSpaceFlowGraph.Factory
)
}
val callback = object : SpaceEntryPoint.Callback {
- override fun onOpenRoom(roomId: RoomId) {
- lambdaError()
- }
+ override fun onOpenRoom(roomId: RoomId, viaParameters: List) = lambdaError()
}
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
.inputs(nodeInputs)
.callback(callback)
.build()
- assertThat(result).isInstanceOf(SpaceNode::class.java)
+ 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/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/SpacePresenterTest.kt
deleted file mode 100644
index 0bcd1303ae..0000000000
--- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/SpacePresenterTest.kt
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Copyright 2025 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-package io.element.android.features.space.impl
-
-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.features.space.api.SpaceEntryPoint
-import io.element.android.libraries.matrix.api.MatrixClient
-import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
-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.libraries.previewutils.room.aSpaceRoom
-import io.element.android.tests.testutils.lambda.lambdaRecorder
-import io.element.android.tests.testutils.test
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-
-class SpacePresenterTest {
- @Test
- fun `present - initial state`() = runTest {
- val paginateResult = lambdaRecorder> {
- Result.success(Unit)
- }
- val presenter = createSpacePresenter(
- client = FakeMatrixClient(
- spaceService = FakeSpaceService(
- spaceRoomListResult = {
- FakeSpaceRoomList(
- paginateResult = paginateResult,
- )
- },
- ),
- ),
- )
- 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()
- advanceUntilIdle()
- paginateResult.assertions().isCalledOnce()
- }
- }
-
- @Test
- fun `present - load more`() = runTest {
- val paginateResult = lambdaRecorder> {
- Result.success(Unit)
- }
- val presenter = createSpacePresenter(
- client = FakeMatrixClient(
- spaceService = FakeSpaceService(
- spaceRoomListResult = {
- FakeSpaceRoomList(
- paginateResult = paginateResult,
- )
- },
- ),
- ),
- )
- 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 fakeSpaceRoomList = FakeSpaceRoomList(
- paginateResult = { Result.success(Unit) },
- )
- val presenter = createSpacePresenter(
- client = FakeMatrixClient(
- spaceService = FakeSpaceService(
- spaceRoomListResult = { fakeSpaceRoomList },
- ),
- ),
- )
- presenter.test {
- val state = awaitItem()
- advanceUntilIdle()
- assertThat(state.hasMoreToLoad).isTrue()
- fakeSpaceRoomList.emitPaginationStatus(
- SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false)
- )
- assertThat(awaitItem().hasMoreToLoad).isFalse()
- fakeSpaceRoomList.emitPaginationStatus(
- SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = true)
- )
- assertThat(awaitItem().hasMoreToLoad).isTrue()
- }
- }
-
- @Test
- fun `present - current space value`() = runTest {
- val fakeSpaceRoomList = FakeSpaceRoomList(
- paginateResult = { Result.success(Unit) },
- )
- val presenter = createSpacePresenter(
- client = FakeMatrixClient(
- spaceService = FakeSpaceService(
- spaceRoomListResult = { fakeSpaceRoomList },
- ),
- ),
- )
- presenter.test {
- val state = awaitItem()
- advanceUntilIdle()
- assertThat(state.currentSpace).isNull()
- val aSpace = aSpaceRoom()
- fakeSpaceRoomList.emitCurrentSpace(aSpace)
- assertThat(awaitItem().currentSpace).isEqualTo(aSpace)
- }
- }
-
- @Test
- fun `present - children value`() = runTest {
- val fakeSpaceRoomList = FakeSpaceRoomList(
- paginateResult = { Result.success(Unit) },
- )
- val presenter = createSpacePresenter(
- client = FakeMatrixClient(
- spaceService = FakeSpaceService(
- spaceRoomListResult = { fakeSpaceRoomList },
- ),
- ),
- )
- presenter.test {
- val state = awaitItem()
- advanceUntilIdle()
- assertThat(state.children).isEmpty()
- val aSpace = aSpaceRoom()
- fakeSpaceRoomList.emitSpaceRooms(listOf(aSpace))
- assertThat(awaitItem().children).containsExactly(aSpace)
- }
- }
-
- private fun createSpacePresenter(
- inputs: SpaceEntryPoint.Inputs = SpaceEntryPoint.Inputs(A_ROOM_ID),
- client: MatrixClient = FakeMatrixClient(),
- seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
- ): SpacePresenter {
- return SpacePresenter(
- inputs = inputs,
- client = client,
- seenInvitesStore = seenInvitesStore,
- )
- }
-}
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..3d123f3f41
--- /dev/null
+++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: 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.AsyncAction
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
+import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom
+import io.element.android.libraries.matrix.api.spaces.SpaceRoom
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
+import io.element.android.libraries.matrix.test.A_ROOM_ID
+import io.element.android.libraries.matrix.test.A_ROOM_ID_2
+import io.element.android.libraries.matrix.test.A_ROOM_ID_3
+import io.element.android.libraries.matrix.test.A_SPACE_ID
+import io.element.android.libraries.matrix.test.A_SPACE_NAME
+import io.element.android.libraries.matrix.test.spaces.FakeLeaveSpaceHandle
+import io.element.android.libraries.previewutils.room.aSpaceRoom
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
+import io.element.android.tests.testutils.test
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class LeaveSpacePresenterTest {
+ private val aSpace = aSpaceRoom(
+ roomId = A_SPACE_ID,
+ displayName = A_SPACE_NAME,
+ )
+
+ @Test
+ fun `present - initial state`() = runTest {
+ val presenter = createLeaveSpacePresenter(
+ leaveSpaceHandle = FakeLeaveSpaceHandle(
+ roomsResult = { Result.success(emptyList()) },
+ ),
+ )
+ presenter.test {
+ val state = awaitItem()
+ assertThat(state.spaceName).isNull()
+ assertThat(state.isLastAdmin).isFalse()
+ assertThat(state.selectableSpaceRooms.isLoading()).isTrue()
+ assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - fail to load rooms`() = runTest {
+ val presenter = createLeaveSpacePresenter(
+ leaveSpaceHandle = FakeLeaveSpaceHandle(
+ roomsResult = { Result.failure(AN_EXCEPTION) },
+ )
+ )
+ presenter.test {
+ val state = awaitItem()
+ assertThat(state.selectableSpaceRooms.isLoading()).isTrue()
+ assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
+ skipItems(3)
+ val stateError = awaitItem()
+ assertThat(stateError.selectableSpaceRooms.isFailure()).isTrue()
+ // Retry
+ stateError.eventSink(LeaveSpaceEvents.Retry)
+ skipItems(1)
+ val stateLoadingAgain = awaitItem()
+ assertThat(stateLoadingAgain.selectableSpaceRooms.isLoading()).isTrue()
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - current space name and is last admin`() = runTest {
+ val presenter = createLeaveSpacePresenter(
+ leaveSpaceHandle = FakeLeaveSpaceHandle(
+ roomsResult = { Result.success(listOf(aLeaveSpaceRoom(spaceRoom = aSpace, isLastAdmin = true))) },
+ )
+ )
+ presenter.test {
+ val state = awaitItem()
+ assertThat(state.spaceName).isNull()
+ skipItems(3)
+ val finalState = awaitItem()
+ assertThat(finalState.spaceName).isEqualTo(A_SPACE_NAME)
+ assertThat(finalState.isLastAdmin).isTrue()
+ // The current state is not in the sub room list
+ assertThat(finalState.selectableSpaceRooms.dataOrNull()!!).isEmpty()
+ }
+ }
+
+ @Test
+ fun `present - direct rooms are filtered out`() = runTest {
+ val leaveResult = lambdaRecorder, Result> { Result.success(Unit) }
+ val presenter = createLeaveSpacePresenter(
+ leaveSpaceHandle = FakeLeaveSpaceHandle(
+ roomsResult = {
+ Result.success(
+ listOf(
+ aLeaveSpaceRoom(spaceRoom = aSpace),
+ aLeaveSpaceRoom(
+ spaceRoom = aSpaceRoom(roomId = A_ROOM_ID, isDirect = false)
+ ),
+ aLeaveSpaceRoom(
+ spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_2, isDirect = true)
+ ),
+ aLeaveSpaceRoom(
+ spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_3, isDirect = null)
+ ),
+ )
+ )
+ },
+ leaveResult = leaveResult,
+ )
+ )
+ presenter.test {
+ val state = awaitItem()
+ assertThat(state.spaceName).isNull()
+ skipItems(3)
+ val finalState = awaitItem()
+ // The current state is not in the sub room list
+ assertThat(finalState.selectableSpaceRooms.dataOrNull()!!.map { it.spaceRoom.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_3)
+ assertThat(finalState.selectedRoomsCount).isEqualTo(2)
+ // Leaving the space will not include the DM
+ finalState.eventSink(LeaveSpaceEvents.LeaveSpace)
+ val stateLeaving = awaitItem()
+ assertThat(stateLeaving.leaveSpaceAction).isEqualTo(AsyncAction.Loading)
+ val stateLeft = awaitItem()
+ assertThat(stateLeft.leaveSpaceAction.isSuccess()).isTrue()
+ leaveResult.assertions().isCalledOnce().with(
+ value(listOf(A_ROOM_ID, A_ROOM_ID_3))
+ )
+ }
+ }
+
+ @Test
+ fun `present - leave space and sub rooms`() = runTest {
+ val leaveResult = lambdaRecorder, Result> { Result.success(Unit) }
+ val presenter = createLeaveSpacePresenter(
+ leaveSpaceHandle = FakeLeaveSpaceHandle(
+ roomsResult = {
+ Result.success(
+ listOf(
+ LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID), isLastAdmin = false),
+ LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID_2), isLastAdmin = true),
+ )
+ )
+ },
+ leaveResult = leaveResult,
+ )
+ )
+ presenter.test {
+ skipItems(4)
+ val state = awaitItem()
+ assertThat(state.spaceName).isNull()
+ assertThat(state.isLastAdmin).isFalse()
+ val data = state.selectableSpaceRooms.dataOrNull()!!
+ assertThat(data.size).isEqualTo(2)
+ // Only one room is selectable as the user is the last admin in the other one
+ val room1 = data[0]
+ assertThat(room1.spaceRoom.roomId).isEqualTo(A_ROOM_ID)
+ assertThat(room1.isSelected).isTrue()
+ assertThat(room1.isLastAdmin).isFalse()
+ val room2 = data[1]
+ assertThat(room2.spaceRoom.roomId).isEqualTo(A_ROOM_ID_2)
+ assertThat(room2.isSelected).isFalse()
+ assertThat(room2.isLastAdmin).isTrue()
+ // Deselect all
+ state.eventSink(LeaveSpaceEvents.DeselectAllRooms)
+ skipItems(1)
+ val stateAllDeselected = awaitItem()
+ val dataAllDeselected = stateAllDeselected.selectableSpaceRooms.dataOrNull()!!
+ assertThat(dataAllDeselected.any { it.isSelected }).isFalse()
+ // Select all
+ stateAllDeselected.eventSink(LeaveSpaceEvents.SelectAllRooms)
+ skipItems(1)
+ val stateAllSelected = awaitItem()
+ val dataAllSelected = stateAllSelected.selectableSpaceRooms.dataOrNull()!!
+ // The last admin room should not be selected
+ assertThat(dataAllSelected.count { it.isSelected }).isEqualTo(1)
+ // Toggle selection of the first room
+ stateAllSelected.eventSink(LeaveSpaceEvents.ToggleRoomSelection(A_ROOM_ID))
+ skipItems(1)
+ val stateOneDeselected = awaitItem()
+ val dataOneDeselected = stateOneDeselected.selectableSpaceRooms.dataOrNull()!!
+ assertThat(dataOneDeselected[0].isSelected).isFalse()
+ // Toggle selection of the first room
+ stateOneDeselected.eventSink(LeaveSpaceEvents.ToggleRoomSelection(A_ROOM_ID))
+ skipItems(1)
+ val stateOneSelected = awaitItem()
+ val dataOneSelected = stateOneSelected.selectableSpaceRooms.dataOrNull()!!
+ assertThat(dataOneSelected[0].isSelected).isTrue()
+ // Leave space
+ stateOneSelected.eventSink(LeaveSpaceEvents.LeaveSpace)
+ val stateLeaving = awaitItem()
+ assertThat(stateLeaving.leaveSpaceAction).isEqualTo(AsyncAction.Loading)
+ val stateLeft = awaitItem()
+ assertThat(stateLeft.leaveSpaceAction.isSuccess()).isTrue()
+ leaveResult.assertions().isCalledOnce().with(
+ value(listOf(A_ROOM_ID))
+ )
+ }
+ }
+
+ @Test
+ fun `present - leave space error and close`() = runTest {
+ val leaveResult = lambdaRecorder, Result> {
+ Result.failure(AN_EXCEPTION)
+ }
+ val presenter = createLeaveSpacePresenter(
+ leaveSpaceHandle = FakeLeaveSpaceHandle(
+ roomsResult = { Result.success(emptyList()) },
+ leaveResult = leaveResult,
+ )
+ )
+ presenter.test {
+ skipItems(4)
+ val state = awaitItem()
+ state.eventSink(LeaveSpaceEvents.LeaveSpace)
+ val stateLeaving = awaitItem()
+ assertThat(stateLeaving.leaveSpaceAction).isEqualTo(AsyncAction.Loading)
+ val stateError = awaitItem()
+ assertThat(stateError.leaveSpaceAction.isFailure()).isTrue()
+ // Close error
+ stateError.eventSink(LeaveSpaceEvents.CloseError)
+ val stateErrorClosed = awaitItem()
+ assertThat(stateErrorClosed.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
+ }
+ }
+
+ private fun createLeaveSpacePresenter(
+ leaveSpaceHandle: LeaveSpaceHandle = FakeLeaveSpaceHandle(),
+ ): LeaveSpacePresenter {
+ return LeaveSpacePresenter(
+ leaveSpaceHandle = leaveSpaceHandle,
+ )
+ }
+}
+
+private fun aLeaveSpaceRoom(
+ spaceRoom: SpaceRoom = aSpaceRoom(
+ roomId = A_SPACE_ID,
+ displayName = A_SPACE_NAME,
+ ),
+ isLastAdmin: Boolean = false,
+) = LeaveSpaceRoom(
+ spaceRoom = spaceRoom,
+ isLastAdmin = isLastAdmin,
+)
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..998868593f
--- /dev/null
+++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: 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.toImmutableList
+import org.junit.Test
+
+class LeaveSpaceStateTest {
+ @Test
+ fun `test loading`() {
+ val sut = aLeaveSpaceState(
+ selectableSpaceRooms = AsyncData.Loading()
+ )
+ assertThat(sut.showQuickAction).isFalse()
+ assertThat(sut.showLeaveButton).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.showLeaveButton).isTrue()
+ assertThat(sut.areAllSelected).isTrue()
+ assertThat(sut.hasOnlyLastAdminRoom).isFalse()
+ assertThat(sut.selectedRoomsCount).isEqualTo(0)
+ }
+
+ @Test
+ fun `test last admin`() {
+ val sut = aLeaveSpaceState(
+ isLastAdmin = true,
+ selectableSpaceRooms = AsyncData.Success(
+ persistentListOf(
+ aSelectableSpaceRoom(isLastAdmin = false, isSelected = false),
+ )
+ )
+ )
+ assertThat(sut.showQuickAction).isFalse()
+ assertThat(sut.showLeaveButton).isFalse()
+ assertThat(sut.areAllSelected).isFalse()
+ 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),
+ ).toImmutableList()
+ )
+ )
+ assertThat(sut.showQuickAction).isTrue()
+ assertThat(sut.showLeaveButton).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),
+ ).toImmutableList()
+ )
+ )
+ assertThat(sut.showQuickAction).isTrue()
+ assertThat(sut.showLeaveButton).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(
+ persistentListOf(
+ aSelectableSpaceRoom(isLastAdmin = true, isSelected = false),
+ aSelectableSpaceRoom(isLastAdmin = false, isSelected = true),
+ aSelectableSpaceRoom(isLastAdmin = false, isSelected = true),
+ )
+ )
+ )
+ assertThat(sut.showQuickAction).isTrue()
+ assertThat(sut.showLeaveButton).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),
+ ).toImmutableList()
+ )
+ )
+ assertThat(sut.showQuickAction).isFalse()
+ assertThat(sut.showLeaveButton).isTrue()
+ 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..fbef7bb3a1
--- /dev/null
+++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt
@@ -0,0 +1,340 @@
+/*
+ * Copyright 2025 New 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())
+ assertThat(state.topicViewerState).isEqualTo(TopicViewerState.Hidden)
+ 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 - topic viewer 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.topicViewerState).isEqualTo(TopicViewerState.Hidden)
+ advanceUntilIdle()
+ state.eventSink(SpaceEvents.ShowTopicViewer("topic"))
+ assertThat(awaitItem().topicViewerState).isEqualTo(TopicViewerState.Shown("topic"))
+ state.eventSink(SpaceEvents.HideTopicViewer)
+ assertThat(awaitItem().topicViewerState).isEqualTo(TopicViewerState.Hidden)
+ }
+ }
+
+ @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..2702133780
--- /dev/null
+++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: 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.matrix.test.A_ROOM_TOPIC
+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
+import org.robolectric.annotation.Config
+
+@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(
+ hasMoreToLoad = false,
+ eventSink = eventsRecorder,
+ ),
+ onBackClick = it,
+ )
+ rule.pressBack()
+ }
+ }
+
+ @Test
+ fun `clicking on a room name invokes the expected callback`() {
+ val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
+ ensureCalledOnceWithParam(aSpaceRoom) {
+ rule.setSpaceView(
+ aSpaceState(
+ children = listOf(aSpaceRoom),
+ hasMoreToLoad = false,
+ 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),
+ hasMoreToLoad = false,
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.clickOn(CommonStrings.action_join)
+ eventsRecorder.assertSingle(SpaceEvents.Join(aSpaceRoom))
+ }
+
+ @Config(qualifiers = "h1024dp")
+ @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(
+ hasMoreToLoad = false,
+ children = listOf(aSpaceRoom),
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.clickOn(CommonStrings.action_accept)
+ eventsRecorder.assertSingle(SpaceEvents.AcceptInvite(aSpaceRoom))
+ }
+
+ @Config(qualifiers = "h1024dp")
+ @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(
+ hasMoreToLoad = false,
+ children = listOf(aSpaceRoom),
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.clickOn(CommonStrings.action_decline)
+ eventsRecorder.assertSingle(SpaceEvents.DeclineInvite(aSpaceRoom))
+ }
+
+ @Config(qualifiers = "h1024dp")
+ @Test
+ fun `clicking on topic emits the expected Event`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setSpaceView(
+ aSpaceState(
+ parentSpace = aSpaceRoom(topic = A_ROOM_TOPIC),
+ hasMoreToLoad = false,
+ eventSink = eventsRecorder,
+ )
+ )
+ rule.onNodeWithText(A_ROOM_TOPIC).performClick()
+ eventsRecorder.assertSingle(SpaceEvents.ShowTopicViewer(A_ROOM_TOPIC))
+ }
+}
+
+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/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 59c8e5bacb..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
@@ -19,7 +19,7 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -36,7 +36,7 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class StartChatFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 cb3a1766fa..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
@@ -14,13 +14,13 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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)
-@Inject
+@AssistedInject
class JoinRoomByAddressNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 42bd54e9f2..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
@@ -17,7 +17,7 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import kotlin.time.Duration.Companion.seconds
private const val ADDRESS_RESOLVE_TIMEOUT_IN_SECONDS = 10
-@Inject
+@AssistedInject
class JoinRoomByAddressPresenter(
@Assisted private val navigator: StartChatNavigator,
private val client: MatrixClient,
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 e5c8f04bbd..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
@@ -17,7 +17,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.features.startchat.StartChatNavigator
@@ -27,7 +27,7 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class StartChatNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 17f5076caf..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
@@ -17,8 +17,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
-import dev.zacsweers.metro.Inject
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,7 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-@Inject
+@AssistedInject
class DefaultUserListPresenter(
@Assisted val args: UserListPresenterArgs,
@Assisted val userRepository: UserRepository,
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/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 77e9959dc5..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
@@ -18,7 +18,7 @@ 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.Assisted
-import dev.zacsweers.metro.Inject
+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
@@ -33,19 +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)
-@Inject
+@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(
@@ -82,7 +82,7 @@ class UserProfileFlowNode(
}
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) {
@@ -99,7 +99,7 @@ class UserProfileFlowNode(
}
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 9a87ea1550..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
@@ -15,7 +15,7 @@ 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.Inject
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
@@ -29,7 +29,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(SessionScope::class)
-@Inject
+@AssistedInject
class UserProfileNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 77786ca096..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
@@ -19,7 +19,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
-@Inject
+@AssistedInject
class UserProfilePresenter(
@Assisted private val userId: UserId,
private val client: MatrixClient,
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
index 1537167d20..75bc434048 100644
--- 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
@@ -19,9 +19,8 @@ import io.element.android.features.verifysession.api.OutgoingVerificationEntryPo
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
-import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
+import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID
-import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode
@@ -43,7 +42,7 @@ class DefaultUserProfileEntryPointTest {
UserProfileFlowNode(
buildContext = buildContext,
plugins = plugins,
- sessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient()),
+ sessionId = A_SESSION_ID,
elementCallEntryPoint = object : ElementCallEntryPoint {
override fun startCall(callType: CallType) = lambdaError()
override suspend fun handleIncomingCall(
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-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/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 cc3d2f1e2d..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
@@ -14,14 +14,14 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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)
-@Inject
+@AssistedInject
class IncomingVerificationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 8be176f117..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
@@ -19,7 +19,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import com.freeletics.flowredux.compose.rememberStateAndDispatch
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 @@ 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
-@Inject
+@AssistedInject
class IncomingVerificationPresenter(
@Assisted private val verificationRequest: VerificationRequest.Incoming,
@Assisted private val navigator: IncomingVerificationNavigator,
@@ -155,7 +155,7 @@ class IncomingVerificationPresenter(
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/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 8813935d0b..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
@@ -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/OutgoingVerificationNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationNode.kt
index c6e8b95230..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
@@ -14,14 +14,14 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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)
-@Inject
+@AssistedInject
class OutgoingVerificationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 84ebec96de..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
@@ -18,7 +18,7 @@ import androidx.compose.runtime.remember
import com.freeletics.flowredux.compose.rememberStateAndDispatch
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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,7 +34,7 @@ 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
-@Inject
+@AssistedInject
class OutgoingVerificationPresenter(
@Assisted private val showDeviceVerifiedScreen: Boolean,
@Assisted private val verificationRequest: VerificationRequest.Outgoing,
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 53cfa97435..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
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-be/translations.xml b/features/verifysession/impl/src/main/res/values-be/translations.xml
index c48da7e8d0..a11cb7d607 100644
--- a/features/verifysession/impl/src/main/res/values-be/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-be/translations.xml
@@ -16,6 +16,7 @@
"Пераканайцеся, што прыведзеныя ніжэй лічбы супадаюць з лічбамі, паказанымі ў іншым сеансе."
"Параўнайце лічбы"
"Ваш новы сеанс пацверджаны. Ён мае доступ да вашых зашыфраваных паведамленняў, і іншыя карыстальнікі будуць лічыць яго давераным."
+ "Прылада праверана"
"Увядзіце ключ аднаўлення"
"Дакажыце, што гэта вы, каб атрымаць доступ да вашай зашыфраванай гісторыі паведамленняў."
"Адкрыйце існуючы сеанс"
@@ -24,6 +25,7 @@
"Чаканне супадзення"
"Параўнайце ўнікальны набор эмодзі."
"Параўнайце ўнікальныя эмодзі, пераканаўшыся, што яны размешчаны ў тым жа парадку."
+ "Ваш новы сеанс пацверджаны. Ён мае доступ да вашых зашыфраваных паведамленняў, і іншыя карыстальнікі будуць лічыць яго давераным."
"Прылада праверана"
"Яны не супадаюць"
"Яны супадаюць"
diff --git a/features/verifysession/impl/src/main/res/values-bg/translations.xml b/features/verifysession/impl/src/main/res/values-bg/translations.xml
index ab0a15f4ed..1a2e194696 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,10 @@
"Устройството е потвърдено"
"Използване на друго устройство"
"Нещо не изглежда наред. Или времето за изчакване на заявката е изтекло, или заявката е отхвърлена."
- "Потвърдете, че емоджитата по-долу съвпадат с показаните в другата ви сесия."
+ "Потвърдете, че емоджитата по-долу съвпадат с показаните в другото ви устройство."
"Сравнете емоджита"
+ "Сега можете да четете или изпращате съобщения сигурно на другото си устройство."
+ "Устройството е потвърдено"
"Въвеждане на ключ за възстановяване"
"Докажете, че сте вие, за да получите достъп до хронологията на шифрованите си съобщения."
"Отворете съществуваща сесия"
@@ -17,6 +19,7 @@
"Готов съм"
"В очакване на съвпадение"
"Сравнете уникален набор от емоджита."
+ "Сравнете уникалните емоджита, като се уверите, че се появяват в същия ред."
"Неуспешно потвърждаване"
"Сега можете да четете или изпращате съобщения сигурно на другото си устройство."
"Устройството е потвърдено"
@@ -24,7 +27,6 @@
"Те съвпадат"
"Уверете се, че приложението е отворено на другото устройство, преди да започнете потвърждението оттук."
"Отворете приложението на друго потвърдено устройство"
- "Чака се другото устройство"
"Чака се другият потребител"
"След като бъдете приети, ще можете да продължите потвърждението."
"Приемете заявката, за да започнете процеса на потвърждаване в другата си сесия, за да продължите."
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-cy/translations.xml b/features/verifysession/impl/src/main/res/values-cy/translations.xml
index eff8ac87d9..2b26322170 100644
--- a/features/verifysession/impl/src/main/res/values-cy/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-cy/translations.xml
@@ -11,13 +11,14 @@
"Defnyddiwch ddyfais arall"
"Yn aros ar ddyfais arall…"
"Mae rhywbeth i weld o\'i le. Naill ai daeth y cais i ben neu cafodd y cais ei wrthod."
- "Cadarnhewch fod yr emojis isod yn cyd-fynd â\'r rhai sy\'n cael eu dangos ar eich sesiwn arall."
+ "Cadarnhewch fod yr emojis isod yn cyd-fynd â\'r rhai sy\'n cael eu dangos ar eich dyfais arall."
"Cymharwch emojis"
"Cadarnhewch fod yr emojis isod yn cyd-fynd â\'r rhai sy\'n cael eu dangos ar ddyfais y defnyddiwr arall."
"Cadarnhewch fod y rhifau isod yn cyfateb i\'r rhai sy\'n cael eu dangos ar eich sesiwn arall."
"Cymharwch rifau"
- "Mae eich sesiwn newydd bellach wedi\'i dilysu. Mae ganddo fynediad i\'ch negeseuon wedi\'u hamgryptio, a bydd defnyddwyr eraill yn ei ystyried yn rhai y mae modd ymddiried ynddyn nhw."
+ "Nawr gallwch chi ddarllen neu anfon negeseuon yn ddiogel ar eich dyfais arall."
"Nawr gallwch ymddiried yn hunaniaeth y defnyddiwr hwn wrth anfon neu dderbyn negeseuon."
+ "Dyfais wedi\'i dilysu"
"Rhowch eich allwedd adfer"
"Naill ai daeth y cais i ben, gwrthodwyd y cais, neu roedd diffyg cyfatebiaeth dilysu."
"Profwch mai chi sydd yno i gael mynediad at eich hanes negeseuon wedi\'u hamgryptio."
@@ -44,7 +45,7 @@
"Er mwyn diogelwch ychwanegol, mae defnyddiwr arall eisiau gwirio pwy ydych chi. Bydd set o emojis yn cael eu dangos i chi eu cymharu."
"Dylech weld llamlen ar y ddyfais arall. Dechreuwch y dilysiad o\'r fan honno nawr."
"Cychwyn dilysu ar y ddyfais arall"
- "Yn aros am eich dyfais arall"
+ "Cychwyn dilysu ar y ddyfais arall"
"Yn aros am y defnyddiwr arall"
"Unwaith y byddwch wedi\'ch derbyn, byddwch yn gallu parhau â\'r dilysu."
"Derbyniwch y cais i gychwyn y broses ddilysu yn eich sesiwn arall i barhau."
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..32cdf159d5 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."
@@ -32,7 +33,7 @@
"Verifikation mislykkedes"
"Fortsæt kun, hvis du selv har startet denne verifikation."
"Verificér den anden enhed for at holde din meddelelseshistorik sikker."
- "Nu kan du læse eller sende beskeder sikkert på din anden enhed."
+ "Nu kan du læse eller sende beskeder sikkert med din anden enhed."
"Enhed verificeret"
"Anmodet om verifikation"
"De matcher ikke"
@@ -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 591d72e65d..377da35af3 100644
--- a/features/verifysession/impl/src/main/res/values-de/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-de/translations.xml
@@ -11,13 +11,14 @@
"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."
- "Bestätige, dass die folgenden Emojis mit denen in deiner anderen Sitzung übereinstimmen."
+ "Bestätige, dass die folgenden Emojis mit denen auf deinem anderen Gerät übereinstimmen."
"Emojis vergleichen"
"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 Sitzung ist nun verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten und wird von anderen Nutzern als vertrauenswürdig eingestuft."
+ "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 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."
@@ -32,7 +33,7 @@
"Verifizierung fehlgeschlagen"
"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."
+ "Jetzt kannst du verschlüsselte Nachrichten sicher auf deinem anderen Gerät schreiben und lesen."
"Gerät verifiziert"
"Verifizierung angefordert"
"Sie stimmen nicht überein"
@@ -44,7 +45,7 @@
"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"
- "Warten auf das andere Gerät"
+ "Starte 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."
diff --git a/features/verifysession/impl/src/main/res/values-el/translations.xml b/features/verifysession/impl/src/main/res/values-el/translations.xml
index 74e926f73d..1cbb7d3c04 100644
--- a/features/verifysession/impl/src/main/res/values-el/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-el/translations.xml
@@ -18,6 +18,7 @@
"Σύγκριση αριθμών"
"Η νέα σου συνεδρία έχει πλέον επαληθευτεί. Έχει πρόσβαση στα κρυπτογραφημένα μηνύματά σας και άλλοι χρήστες θα το βλέπουν ως αξιόπιστο."
"Τώρα μπορείτε να εμπιστευτείτε την ταυτότητα αυτού του χρήστη κατά την αποστολή ή τη λήψη μηνυμάτων."
+ "Επαληθευμένη συσκευή"
"Εισαγωγή κλειδιού ανάκτησης"
"Είτε το αίτημα έληξε είτε απορρίφθηκε είτε υπήρξε αναντιστοιχία επαλήθευσης."
"Απέδειξε ότι είσαι εσύ για να αποκτήσεις πρόσβαση στο κρυπτογραφημένο ιστορικό μηνυμάτων σου."
@@ -32,7 +33,7 @@
"Αποτυχία επαλήθευσης"
"Συνέχισε μόνο εάν ξεκίνησες εσύ αυτήν την επαλήθευση."
"Επαλήθευσε την άλλη συσκευή για να διατηρήσεις το ιστορικό μηνυμάτων σου ασφαλές."
- "Τώρα μπορείς να διαβάσεις ή να στείλεις μηνύματα με ασφάλεια στην άλλη συσκευή σου."
+ "Η νέα σου συνεδρία έχει πλέον επαληθευτεί. Έχει πρόσβαση στα κρυπτογραφημένα μηνύματά σας και άλλοι χρήστες θα το βλέπουν ως αξιόπιστο."
"Επαληθευμένη συσκευή"
"Ζητήθηκε επαλήθευση"
"Δεν ταιριάζουν"
@@ -44,7 +45,7 @@
"Για επιπλέον ασφάλεια, ένας άλλος χρήστης θέλει να επαληθεύσει την ταυτότητά σας. Θα σας εμφανιστεί μια σειρά από emojis για να τα συγκρίνετε."
"Πρόκειται να δεις ένα αναδυόμενο παράθυρο στην άλλη συσκευή. Ξεκίνα την επαλήθευση από εκεί τώρα."
"Έναρξη επαλήθευσης στην άλλη συσκευή"
- "Αναμονή για την άλλη συσκευή"
+ "Έναρξη επαλήθευσης στην άλλη συσκευή"
"Αναμονή για τον άλλο χρήστη"
"Μόλις γίνει αποδεκτό, θα μπορείτε να συνεχίσετε με την επαλήθευση."
"Αποδέξου το αίτημα για να ξεκινήσεις τη διαδικασία επαλήθευσης στην άλλη συνεδρία σου για να συνεχίσεις."
diff --git a/features/verifysession/impl/src/main/res/values-eo/translations.xml b/features/verifysession/impl/src/main/res/values-eo/translations.xml
index d4b40184a1..71beec704e 100644
--- a/features/verifysession/impl/src/main/res/values-eo/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-eo/translations.xml
@@ -5,8 +5,12 @@
"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."
+ "Device confirmed"
"Enter backup password"
+ "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted."
"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."
diff --git a/features/verifysession/impl/src/main/res/values-es/translations.xml b/features/verifysession/impl/src/main/res/values-es/translations.xml
index 0f92e96395..19d471cb2b 100644
--- a/features/verifysession/impl/src/main/res/values-es/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-es/translations.xml
@@ -18,6 +18,7 @@
"Comparar números"
"Tu nueva sesión ya está verificada. Tienes acceso a tus mensajes cifrados y otros usuarios lo considerarán de confianza."
"Ahora puedes confiar en la identidad de este usuario al enviar o recibir mensajes."
+ "Dispositivo verificado"
"Introduce la clave de recuperación"
"O bien se agotó el tiempo de solicitud, se rechazó la solicitud o hubo una discrepancia en la verificación."
"Demuestra que eres tú para acceder a tu historial de mensajes cifrados."
@@ -32,7 +33,7 @@
"Verificación fallida"
"Continúa solo si has iniciado esta verificación."
"Verifica el otro dispositivo para mantener seguro tu historial de mensajes."
- "Ahora puedes leer o enviar mensajes de forma segura en tu otro dispositivo."
+ "Tu nueva sesión ya está verificada. Tienes acceso a tus mensajes cifrados y otros usuarios lo considerarán de confianza."
"Dispositivo verificado"
"Verificación solicitada"
"No coinciden"
@@ -44,7 +45,7 @@
"Para mayor seguridad, otro usuario quiere verificar tu identidad. Se te mostrará un conjunto de emojis para compararlos."
"Deberías ver una ventana emergente en el otro dispositivo. Inicia ahora la verificación desde allí."
"Iniciar la verificación en el otro dispositivo"
- "A la espera del otro dispositivo"
+ "Iniciar la verificación en el otro dispositivo"
"A la espera del otro usuario"
"Una vez aceptada, podrás continuar con la verificación."
"Acepta la solicitud para iniciar el proceso de verificación en tu otra sesión para continuar."
diff --git a/features/verifysession/impl/src/main/res/values-et/translations.xml b/features/verifysession/impl/src/main/res/values-et/translations.xml
index fa2a5733da..2c43794b72 100644
--- a/features/verifysession/impl/src/main/res/values-et/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-et/translations.xml
@@ -16,8 +16,9 @@
"Palun kinnita, et allpool näidatud emojid vastavad täpselt neile, mida kuvatakse teise kasutaja seadmes."
"Kinnita, et kõik järgnevalt kuvatud numbrid on täpselt samad, mida sa näed oma teises sessioonis."
"Võrdle numbreid"
- "Sinu uus sessioon on nüüd verifitseeritud. Sellel sessioonil on nüüd ligipääs sinu krüptitud sõnumitele ja teised osapooled näevad teda usaldusväärsena."
+ "Võid nüüd sõnumeid oma teises seadmes turvaliselt saata ja vastu võtta."
"Nüüd sa võid sõnumite vastuvõtmisel ja saatmisel selle kasutaja identiteeti usaldada."
+ "Seade on verifitseeritud"
"Sisesta taastevõti"
"Kas verifitseerimine aegus, teine osapool keeldus vastamast või tekkis vastuste mittevastavus."
"Saamaks ligipääsu krüptitud sõnumite ajaloole tõesta et tegemist on sinuga."
@@ -44,7 +45,7 @@
"Lisaturvalisuse nimel soovib teine kasutaja sinu identiteeti verifitseerida. Järgmiseks näed sa emojisid, mida peate omavahel võrdlema."
"Sa peaksid teises seadmes nägema hüpikakent. Palun alusta sealt verifitseerimist."
"Alusta verifitseerimist teises seadmes"
- "Ootame teise seadme järgi"
+ "Alusta verifitseerimist teises seadmes"
"Ootame teise kasutaja järgi"
"Kui oled nõustunud, siis saad sa verifitseerimist jätkata."
"Jätkamaks nõustu verifitseerimisprotsessi alustamisega oma teises sessioonis."
diff --git a/features/verifysession/impl/src/main/res/values-eu/translations.xml b/features/verifysession/impl/src/main/res/values-eu/translations.xml
index 2497e73069..c6c821a0b4 100644
--- a/features/verifysession/impl/src/main/res/values-eu/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-eu/translations.xml
@@ -15,6 +15,7 @@
"Egiaztatu beheko zenbakiak zure beste saioan erakutsitakoekin bat datozela."
"Konparatu zenbakiak"
"Saio berria egiaztatu da. Zifratutako mezu guztiak atzitu ditzake, eta gainerako erabiltzaileek fidagarritzat izango dute."
+ "Gailua egiaztatu da"
"Sartu berreskuratze-gakoa"
"Frogatu zeu zarela zifratutako mezuen historia atzitzeko."
"Ireki lehendik hasita dagoen saio bat"
@@ -26,7 +27,7 @@
"Egiaztapenak huts egin du"
"Egiaztapen hau zeuk hasi baduzu bakarrik jarraitu."
"Egiaztatu beste gailua zure mezuen historia seguru mantentzeko."
- "Orain mezuak modu seguruan irakurri edo bidal ditzakezu beste gailuan."
+ "Saio berria egiaztatu da. Zifratutako mezu guztiak atzitu ditzake, eta gainerako erabiltzaileek fidagarritzat izango dute."
"Gailua egiaztatu da"
"Egiaztapena eskatu da"
"Ez datoz bat"
@@ -38,7 +39,7 @@
"Segurtasun handiagorako, beste erabiltzaile batek zure identitatea egiaztatu nahi du. Emoji sorta bat erakutsiko zaizu konparatzeko."
"Beste gailuan laster-menu bat ikusi beharko zenuke. Hasi egiaztapena hortik orain."
"Hasi egiaztapena beste gailuan"
- "Beste gailuaren zain"
+ "Hasi egiaztapena beste gailuan"
"Beste erabiltzailearen zain"
"Onartutakoan egiaztapenarekin jarraitu ahal izango duzu."
"Jarraitzeko, onartu zure beste saioan egiaztapen-prozesua hasteko eskaera."
diff --git a/features/verifysession/impl/src/main/res/values-fa/translations.xml b/features/verifysession/impl/src/main/res/values-fa/translations.xml
index 4395789a51..ace791d1b0 100644
--- a/features/verifysession/impl/src/main/res/values-fa/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-fa/translations.xml
@@ -15,6 +15,7 @@
"مقایسهٔ شکلکها"
"مقایسهٔ اعداد"
"اکنون نشست جدیدتان تأیید شده. این نشست به پیامهای رمزنگارش شدهتان دسترسی داشته و دیگر کاربران مطمئن میبینندش."
+ "افزاره تأیید شده"
"ورود کلید بازیابی"
"برای دسترسی به تاریخچه پیامهای رمزگذاریشدهتان، ثابت کنید که خودتان هستید."
"گشودن نشستی موجود"
@@ -24,6 +25,7 @@
"مقایسهٔ مجموعهای یکتا از شکلکها."
"شکلکها را مقایسه کنید، از ترتیب نمایش آنان نیز مطمئن شوید."
"صحتسنجی شکست خورد"
+ "اکنون نشست جدیدتان تأیید شده. این نشست به پیامهای رمزنگارش شدهتان دسترسی داشته و دیگر کاربران مطمئن میبینندش."
"افزاره تأیید شده"
"مطابق نیستند"
"مطابقند"
diff --git a/features/verifysession/impl/src/main/res/values-fi/translations.xml b/features/verifysession/impl/src/main/res/values-fi/translations.xml
index 49e09aab99..4ee897ba92 100644
--- a/features/verifysession/impl/src/main/res/values-fi/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-fi/translations.xml
@@ -16,8 +16,9 @@
"Vahvista, että alla olevat hymiöt vastaavat toisen käyttäjän laitteessa näkyviä hymiöitä."
"Varmista, että alla olevat numerot vastaavat toisessa istunnossa näkyviä numeroita."
"Vertaa numeroita"
- "Uusi kirjautumisesi on nyt vahvistettu. Sillä on pääsy salattuihin viesteihisi, ja muut käyttäjät näkevät sen luotettuna."
+ "Nyt voit lukea tai lähettää viestejä turvallisesti toisella laitteellasi."
"Nyt voit luottaa tämän käyttäjän identiteettiin, kun lähetät tai vastaanotat viestejä."
+ "Laite vahvistettu"
"Syötä palautusavain"
"Joko pyyntö aikakatkaistiin, pyyntö hylättiin tai vahvistus ei täsmännyt."
"Vahvista, että se olet sinä, jotta näet aiemmat salatut viestisi."
@@ -44,7 +45,7 @@
"Toinen käyttäjä haluaa vahvistaa identiteettisi turvallisuuden lisäämiseksi. Sinulle näytetään joukko emojeja vertailtavaksi."
"Sinun pitäisi nähdä ponnahdusikkuna toisessa laitteessa. Aloita vahvistus nyt sieltä."
"Aloita vahvistus toisella laitteella"
- "Odotetaan toista laitetta"
+ "Aloita vahvistus toisella laitteella"
"Odotetaan toista käyttäjää"
"Kun pyyntö on hyväksytty, voit jatkaa vahvistusta."
"Hyväksy vahvistuspyyntö toisella laitteella jatkaaksesi."
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..7417a10bfa 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."
+ "Session vérifiée"
"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-in/translations.xml b/features/verifysession/impl/src/main/res/values-in/translations.xml
index 655520e51f..e53c3339e5 100644
--- a/features/verifysession/impl/src/main/res/values-in/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-in/translations.xml
@@ -18,6 +18,7 @@
"Bandingkan angka"
"Sesi baru Anda sekarang diverifikasi. Ini memiliki akses ke pesan terenkripsi Anda, dan pengguna lain akan melihatnya sebagai tepercaya."
"Sekarang Anda dapat mempercayai identitas pengguna ini saat mengirim atau menerima pesan."
+ "Perangkat terverifikasi"
"Masukkan kunci pemulihan"
"Entah permintaan habis waktu, permintaan ditolak, atau ada ketidakcocokan verifikasi."
"Buktikan bahwa ini memang Anda untuk mengakses riwayat pesan terenkripsi Anda."
@@ -32,7 +33,7 @@
"Verifikasi gagal"
"Lanjutkan hanya jika Anda memulai verifikasi ini."
"Verifikasi perangkat lain untuk menjaga riwayat pesan Anda tetap aman."
- "Sekarang Anda dapat membaca atau mengirim pesan dengan aman di perangkat Anda yang lain."
+ "Sesi baru Anda sekarang diverifikasi. Ini memiliki akses ke pesan terenkripsi Anda, dan pengguna lain akan melihatnya sebagai tepercaya."
"Perangkat terverifikasi"
"Verifikasi diminta"
"Mereka tidak cocok"
@@ -44,7 +45,7 @@
"Untuk keamanan tambahan, pengguna lain ingin memverifikasi identitas Anda. Anda akan ditampilkan satu set emoji untuk dibandingkan."
"Anda akan melihat popup di perangkat lain. Mulai verifikasi dari sana sekarang."
"Mulai verifikasi di perangkat lain"
- "Menunggu perangkat lain"
+ "Mulai verifikasi di perangkat lain"
"Menunggu pengguna lain"
"Setelah diterima, Anda akan dapat melanjutkan verifikasi."
"Terima permintaan untuk memulai proses verifikasi di sesi Anda yang lain untuk melanjutkan."
diff --git a/features/verifysession/impl/src/main/res/values-it/translations.xml b/features/verifysession/impl/src/main/res/values-it/translations.xml
index bb55310d31..3be984865e 100644
--- a/features/verifysession/impl/src/main/res/values-it/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-it/translations.xml
@@ -18,6 +18,7 @@
"Confronta i numeri"
"La tua nuova sessione è ora verificata. Ha accesso ai tuoi messaggi crittografati e gli altri utenti la vedranno come attendibile."
"Ora puoi fidarti dell\'identità di questo utente quando invii o ricevi messaggi."
+ "Dispositivo verificato"
"Inserisci la chiave di recupero"
"La richiesta è scaduta, è stata rifiutata o c\'è stata una mancata corrispondenza nella verifica."
"Dimostra la tua identità per accedere alla cronologia dei messaggi crittografati."
@@ -32,7 +33,7 @@
"Verifica fallita"
"Continua solo se tu hai avviato questa verifica."
"Verifica l\'altro dispositivo per proteggere la cronologia dei messaggi."
- "Ora puoi leggere o inviare messaggi in modo sicuro sull\'altro dispositivo."
+ "La tua nuova sessione è ora verificata. Ha accesso ai tuoi messaggi crittografati e gli altri utenti la vedranno come attendibile."
"Dispositivo verificato"
"Richiesta di verifica"
"Non corrispondono"
@@ -44,7 +45,7 @@
"Per una maggiore sicurezza, un altro utente desidera verificare la tua identità. Ti verrà mostrato un set di emoji da confrontare."
"Dovresti vedere un popup sull\'altro dispositivo. Inizia subito la verifica da lì."
"Avvia la verifica sull\'altro dispositivo"
- "In attesa dell\'altro dispositivo"
+ "Avvia la verifica sull\'altro dispositivo"
"In attesa dell\'altro utente"
"Una volta accettata, potrai proseguire con la verifica."
"Accetta la richiesta di avviare il processo di verifica nell\'altra sessione per continuare."
diff --git a/features/verifysession/impl/src/main/res/values-ka/translations.xml b/features/verifysession/impl/src/main/res/values-ka/translations.xml
index 5815ecefaa..eed71a3b61 100644
--- a/features/verifysession/impl/src/main/res/values-ka/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-ka/translations.xml
@@ -12,6 +12,7 @@
"დაადასტურეთ, რომ ქვემოთ მოცემული ნომრები ემთხვევა თქვენს სხვა სესიაზე ნაჩვენები ნომრებს."
"შეადარეთ რიცხვები"
"თქვენი ახალი სესია დადასტურებულია. მას აქვს წვდომა დაშიფრულ შეტყობინებებზე და სხვა მომხმარებლები მას სანდოდ ხედავენ."
+ "მოწყობილობა დადასტურებულია"
"შეიყვანეთ აღდგენის გასაღები"
"დაამტკიცეთ, რომ ეს თქვენ ხართ, რათა მიიღოთ წვდომა თქვენი დაშიფრული შეტყობინებების ისტორიასთან."
"არსებული სესიის გახსნა"
@@ -20,6 +21,7 @@
"ველოდებით დამთხვევას"
"შეადარეთ ემოციების უნიკალური ნაკრები."
"შეადარეთ უნიკალური ემოჯი, დარწმუნდით, რომ ისინი ერთი დ იმავე თანმიმდევრობით გამოჩნდნენ."
+ "თქვენი ახალი სესია დადასტურებულია. მას აქვს წვდომა დაშიფრულ შეტყობინებებზე და სხვა მომხმარებლები მას სანდოდ ხედავენ."
"მოწყობილობა დადასტურებულია"
"ისინი არ ემთხვევიან ერთმანეთს"
"ისინი ემთხვევიან ერთმანეთს"
diff --git a/features/verifysession/impl/src/main/res/values-ko/translations.xml b/features/verifysession/impl/src/main/res/values-ko/translations.xml
index f04ba0ba08..1dfe3f752a 100644
--- a/features/verifysession/impl/src/main/res/values-ko/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-ko/translations.xml
@@ -18,6 +18,7 @@
"숫자 비교"
"새로운 세션이 확인되었습니다. 이 세션은 귀하의 암호화된 메시지에 액세스할 수 있으며, 다른 사용자는 이 세션을 신뢰할 수 있는 세션으로 인식합니다."
"이제 메시지를 보내거나 받을 때 이 사용자의 신원을 신뢰할 수 있습니다."
+ "기기 검증됨"
"복구 키를 입력하세요"
"요청이 시간 초과되었거나, 요청이 거부되었거나, 검증 불일치가 발생했습니다."
"암호화된 메시지 기록에 액세스하기 위해 본인임을 증명하세요."
@@ -32,7 +33,7 @@
"검증 실패"
"본인이 이 검증을 시작한 경우에만 계속 진행하세요."
"다른 기기를 확인하여 메시지 기록을 안전하게 보호하세요."
- "이제 다른 기기에서도 안전하게 메시지를 읽거나 보낼 수 있습니다."
+ "새로운 세션이 확인되었습니다. 이 세션은 귀하의 암호화된 메시지에 액세스할 수 있으며, 다른 사용자는 이 세션을 신뢰할 수 있는 세션으로 인식합니다."
"기기 검증됨"
"검증 요청"
"일치하지 않습니다"
@@ -44,7 +45,7 @@
"추가 보안 위해 다른 사용자가 귀하의 신원을 확인하고자 합니다. 비교할 이모티콘 세트가 표시됩니다."
"다른 기기에 팝업이 표시될 것입니다. 지금 그곳에서 확인을 시작하세요."
"다른 장치에서 검증 시작"
- "다른 기기를 기다리고 있습니다"
+ "다른 장치에서 검증 시작"
"다른 사용자를 기다리는 중"
"승인 후에는 검증 과정을 계속 진행할 수 있습니다."
"계속하려면 다른 세션에서 검증 과정을 시작하라는 요청을 수락하세요."
diff --git a/features/verifysession/impl/src/main/res/values-lt/translations.xml b/features/verifysession/impl/src/main/res/values-lt/translations.xml
index 0fd7a2e755..34ea11d141 100644
--- a/features/verifysession/impl/src/main/res/values-lt/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-lt/translations.xml
@@ -10,6 +10,7 @@
"Aš pasiruošęs"
"Laukiama atitikimo…"
"Palyginkite unikalius jaustukus, įsitikindami, kad jie rodomi ta pačia tvarka."
+ "Jūsų nauja sesija dabar patvirtinta. Ji turi prieigą prie jūsų užšifruotų pranešimų, o kiti vartotojai matys ją kaip patikimą."
"Jie nesutampa"
"Jie sutampa"
"Kitoje sesijoje priimkite prašymą pradėti tikrinimo procesą, kad galėtumėte tęsti."
diff --git a/features/verifysession/impl/src/main/res/values-nb/translations.xml b/features/verifysession/impl/src/main/res/values-nb/translations.xml
index afb8298a20..533a0fde1d 100644
--- a/features/verifysession/impl/src/main/res/values-nb/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-nb/translations.xml
@@ -11,13 +11,14 @@
"Bruk en annen enhet"
"Venter på en annen enhet…"
"Noe virker ikke riktig. Enten ble forespørselen tidsavbrutt eller forespørselen ble avslått."
- "Bekreft at emojiene nedenfor samsvarer med de som vises på den andre sesjonen din."
+ "Bekreft at emojiene nedenfor samsvarer med de som vises på den andre enheten din."
"Sammenlign emojier"
"Bekreft at emojiene nedenfor stemmer overens med de som vises på den andre brukerens enhet."
"Kontroller at tallene nedenfor stemmer overens med dem som vises på den andre sesjonen."
"Sammenlign tallene"
- "Den nye sesjonen din er nå bekreftet. Den har tilgang til de krypterte meldingene dine, og andre brukere vil se den som klarert."
+ "Nå kan du lese eller sende meldinger sikkert på den andre enheten din."
"Nå kan du stole på identiteten til denne brukeren når du sender eller mottar meldinger."
+ "Enhet verifisert"
"Skriv inn gjenopprettingsnøkkel"
"Enten ble forespørselen tidsavbrutt, forespørselen ble avslått eller det var en feil i verifiseringen."
"Bevis at det er deg for å få tilgang til den krypterte meldingshistorikken din."
@@ -44,7 +45,7 @@
"For ekstra sikkerhet vil en annen bruker bekrefte identiteten din. Du får se et sett med emojier som du må sammenligne."
"Du skal se en popup på den andre enheten. Start bekreftelsen derfra nå."
"Start verifiseringen på den andre enheten"
- "Venter på den andre enheten"
+ "Start verifiseringen på den andre enheten"
"Venter på den andre brukeren"
"Når du er akseptert, kan du fortsette med verifiseringen."
"Godta forespørselen om å starte bekreftelsesprosessen i den andre sesjonen for å fortsette."
diff --git a/features/verifysession/impl/src/main/res/values-nl/translations.xml b/features/verifysession/impl/src/main/res/values-nl/translations.xml
index 54797b1244..d7bc1d4172 100644
--- a/features/verifysession/impl/src/main/res/values-nl/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-nl/translations.xml
@@ -16,6 +16,7 @@
"Bevestig dat de onderstaande cijfers overeenkomen met de cijfers die worden weergegeven in je andere sessie."
"Vergelijk getallen"
"Je nieuwe sessie is nu geverifieerd. Het heeft toegang tot je versleutelde berichten en andere gebruikers zullen het als vertrouwd beschouwen."
+ "Apparaat geverifieerd"
"Voer herstelsleutel in"
"Bewijs dat jij het bent om toegang te krijgen tot je versleutelde berichtgeschiedenis."
"Open een bestaande sessie"
@@ -27,6 +28,7 @@
"Ingelogd"
"Ga alleen verder als je deze verificatie hebt gestart."
"Verifieer het andere apparaat om je berichtengeschiedenis veilig te houden."
+ "Je nieuwe sessie is nu geverifieerd. Het heeft toegang tot je versleutelde berichten en andere gebruikers zullen het als vertrouwd beschouwen."
"Apparaat geverifieerd"
"Verificatieverzocht"
"Ze komen niet overeen"
diff --git a/features/verifysession/impl/src/main/res/values-pl/translations.xml b/features/verifysession/impl/src/main/res/values-pl/translations.xml
index d5cd461b8d..f76f2c31fa 100644
--- a/features/verifysession/impl/src/main/res/values-pl/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-pl/translations.xml
@@ -18,6 +18,7 @@
"Porównaj liczby"
"Twoja nowa sesja jest teraz zweryfikowana. Ma ona dostęp do Twoich zaszyfrowanych wiadomości, a inni użytkownicy będą widzieć ją jako zaufaną."
"Teraz możesz zaufać tożsamości tego użytkownika podczas wysyłania lub odbierania wiadomości."
+ "Urządzenie zweryfikowane"
"Wprowadź klucz przywracania"
"Albo upłynął limit czasu żądania, albo żądanie zostało odrzucone, albo wystąpił błąd weryfikacji."
"Udowodnij, że to ty, aby uzyskać dostęp do historii zaszyfrowanych wiadomości."
@@ -32,7 +33,7 @@
"Weryfikacja nie powiodła się"
"Kontynuuj tylko, jeśli to Ty zainicjowałeś tę weryfikację."
"Zweryfikuj drugie urządzenie, aby zabezpieczyć historię wiadomości."
- "Już możesz bezpiecznie czytać lub wysyłać wiadomości na drugim urządzeniu."
+ "Twoja nowa sesja jest teraz zweryfikowana. Ma ona dostęp do Twoich zaszyfrowanych wiadomości, a inni użytkownicy będą widzieć ją jako zaufaną."
"Urządzenie zweryfikowane"
"Zażądano weryfikacji"
"Nie pasują do siebie"
@@ -44,7 +45,7 @@
"Dla dodatkowej ochrony, inny użytkownik chce zweryfikować Twoją tożsamość. Pojawi się unikalny zestaw emoji do porównania."
"Powinno wyskoczyć okno na drugim urządzeniu. Rozpocznij tam weryfikację."
"Rozpocznij weryfikację na drugim urządzeniu"
- "Oczekiwanie na drugie urządzenie"
+ "Rozpocznij weryfikację na drugim urządzeniu"
"Oczekiwanie na drugiego użytkownika"
"Po zaakceptowaniu będziesz mógł kontynuować weryfikację."
"Zaakceptuj prośbę o rozpoczęcie procesu weryfikacji w innej sesji, aby kontynuować."
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 ad3b15869a..8dd7c9a2f7 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
@@ -18,6 +18,7 @@
"Comparar números"
"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."
+ "Dispositivo verificado"
"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."
@@ -32,7 +33,7 @@
"A verificação falhou"
"Continue somente se você iniciou esta verificação."
"Verifique o outro dispositivo para manter seu histórico de mensagens seguro."
- "Agora você pode ler ou enviar mensagens com segurança em seu outro dispositivo."
+ "Sua nova sessão está verificada agora. Ela tem acesso às suas mensagens criptografadas e outros usuários a verão como confiável."
"Dispositivo verificado"
"Verificação solicitada"
"Eles não combinam"
@@ -44,7 +45,7 @@
"Para maior segurança, outro usuário deseja verificar sua identidade. Você verá um conjunto de emojis para comparar."
"Você deverá ver um pop-up no outro dispositivo. Você deverá ver uma janela pop-up no outro dispositivo e iniciar a verificação a partir daí."
"Inicie a verificação no outro dispositivo"
- "Aguardando o outro dispositivo"
+ "Inicie a verificação no 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 na sua outra sessão para continuar."
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 ce2eb2af15..bf32d3c7b9 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."
@@ -32,7 +33,7 @@
"A verificação falhou"
"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."
+ "Agora já podes ler ou enviar mensagens com segurança a partir do teu outro dispositivo."
"Dispositivo verificado"
"Verificação solicitada"
"Não correspondem"
@@ -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 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 08064b056a..0d1ddd0530 100644
--- a/features/verifysession/impl/src/main/res/values-ro/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-ro/translations.xml
@@ -16,8 +16,9 @@
"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."
+ "Noua dumneavoastră sesiune este acum verificată. Are acces la mesajele dumneavoastră criptate, iar ceilalti utilizatori vă vor vedea ca fiind de încredere."
"Acum puteți avea încredere în identitatea acestui utilizator atunci când trimiteți sau primiți mesaje."
+ "Dispozitiv verificat"
"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 mesaje anterioare criptate."
@@ -32,7 +33,7 @@
"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 mesajele anterioare în siguranță."
- "Acum puteți citi sau trimite mesaje în siguranță pe celălalt dispozitiv."
+ "Noua dumneavoastră sesiune este acum verificată. Are acces la mesajele dumneavoastră criptate, iar ceilalti utilizatori vă vor vedea ca fiind de încredere."
"Dispozitiv verificat"
"Verificare cerută"
"Nu se potrivesc"
@@ -44,7 +45,7 @@
"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"
+ "Începeți verificarea pe 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."
diff --git a/features/verifysession/impl/src/main/res/values-ru/translations.xml b/features/verifysession/impl/src/main/res/values-ru/translations.xml
index ace2354837..0c2347a2ea 100644
--- a/features/verifysession/impl/src/main/res/values-ru/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-ru/translations.xml
@@ -11,13 +11,14 @@
"Использовать другое устройство"
"Ожидание на другом устройстве…"
"Похоже, что-то не так. Время ожидания запроса либо истекло, либо запрос был отклонен."
- "Убедитесь, что приведенные ниже емоджи совпадают с емоджи показанными во время другого сеанса."
+ "Убедитесь, что приведенные ниже эмодзи совпадают с эмодзи показанными на другом устройстве."
"Сравните емодзи"
"Убедитесь, что указанные ниже эмодзи соответствуют тем, которые отображаются на устройстве другого пользователя."
"Убедитесь, что приведенные ниже числа совпадают с цифрами, показанными в другом сеансе."
"Сравните числа"
- "Ваш новый сеанс подтвержден. У него есть доступ к вашим зашифрованным сообщениям, и другие пользователи увидят его как доверенное."
+ "Теперь вы можете читать или отправлять сообщения безопасно в другом вашем устройстве."
"Теперь вы можете доверять этому пользователя при отправке или получении сообщений."
+ "Устройство проверено"
"Введите ключ восстановления"
"Время ожидания подтверждения истекло, запрос был отклонён, или при подтверждении произошло несоответствие."
"Чтобы получить доступ к зашифрованной истории сообщений, докажите, что это вы."
@@ -32,7 +33,7 @@
"Сбой проверки"
"Продолжайте только если вы ожидали данное подтверждение."
"Чтобы сохранить историю сообщений в безопасности, проверьте другое устройство."
- "Теперь вы можете безопасно читать или отправлять сообщения на другом устройстве."
+ "Теперь вы можете читать или отправлять сообщения безопасно в другом вашем устройстве."
"Устройство проверено"
"Запрошено подтверждение"
"Они не совпадают"
@@ -44,7 +45,7 @@
"Для дополнительной безопасности другой пользователь хочет проверить вашу личность. Вам будет показан набор эмодзи для сравнения."
"Вы должны увидеть всплывающее окно на другом устройстве. Начните проверку оттуда прямо сейчас."
"Начать проверку на другом устройстве"
- "Ожидание другого устройства"
+ "Начать проверку на другом устройстве"
"Ожидание другого пользователя"
"После одобрения вы сможете продолжить проверку."
"Чтобы продолжить, примите запрос на запуск процесса подтверждения в другом сеансе."
diff --git a/features/verifysession/impl/src/main/res/values-sk/translations.xml b/features/verifysession/impl/src/main/res/values-sk/translations.xml
index cc5f26e85b..7c4781db77 100644
--- a/features/verifysession/impl/src/main/res/values-sk/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-sk/translations.xml
@@ -18,6 +18,7 @@
"Porovnať čísla"
"Vaša nová relácia je teraz overená. Má prístup k vašim zašifrovaným správam a ostatní používatelia ju budú vidieť ako dôveryhodnú."
"Teraz môžete dôverovať identite tohto používateľa pri odosielaní alebo prijímaní správ."
+ "Zariadenie overené"
"Zadajte kľúč na obnovenie"
"Buď žiadosť vypršala, žiadosť bola zamietnutá, alebo došlo k nesúladu overovania."
"Dokážte, že ste to vy, aby ste získali prístup k histórii vašich zašifrovaných správ."
@@ -32,7 +33,7 @@
"Overenie zlyhalo"
"Pokračujte iba vtedy, ak ste toto overenie začali."
"Overte druhé zariadenie, aby bola vaša história správ zabezpečená."
- "Teraz môžete bezpečne čítať alebo odosielať správy na svojom druhom zariadení."
+ "Vaša nová relácia je teraz overená. Má prístup k vašim zašifrovaným správam a ostatní používatelia ju budú vidieť ako dôveryhodnú."
"Zariadenie overené"
"Vyžadované overenie"
"Nezhodujú sa"
@@ -44,7 +45,7 @@
"Kvôli vyššej bezpečnosti chce druhý používateľ overiť vašu identitu. Zobrazí sa vám sada emotikonov na porovnanie."
"Na druhom zariadení by sa malo zobraziť vyskakovacie okno. Začnite teraz overovanie odtiaľ."
"Spustiť overovanie na druhom zariadení"
- "Čaká sa na druhé zariadenie"
+ "Spustiť overovanie na druhom zariadení"
"Čaká sa na druhého používateľa"
"Po prijatí budete môcť pokračovať v overovaní."
"Ak chcete pokračovať, prijmite žiadosť o spustenie procesu overenia vo vašej druhej relácii."
diff --git a/features/verifysession/impl/src/main/res/values-sv/translations.xml b/features/verifysession/impl/src/main/res/values-sv/translations.xml
index 0fd66a82d3..3e92ab0189 100644
--- a/features/verifysession/impl/src/main/res/values-sv/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-sv/translations.xml
@@ -18,6 +18,7 @@
"Jämför siffror"
"Din nya session är nu verifierad. Den har tillgång till dina krypterade meddelanden, och andra användare kommer att se den som betrodd."
"Nu kan du lita på användarens identitet när du skickar eller tar emot meddelanden."
+ "Enhet verifierad"
"Ange återställningsnyckel"
"Antingen överskreds tidsgränsen för begäran, begäran nekades eller så fanns det ett matchningsfel för verifieringen."
"Bevisa att det är du för att komma åt din krypterade meddelandehistorik."
@@ -32,7 +33,7 @@
"Verifiering misslyckades"
"Fortsätt bara om du initierade denna verifiering."
"Verifiera den andra enheten för att hålla din meddelandehistorik säker."
- "Du kan nu läsa eller skicka meddelanden säkert på din andra enhet."
+ "Din nya session är nu verifierad. Den har tillgång till dina krypterade meddelanden, och andra användare kommer att se den som betrodd."
"Enhet verifierad"
"Verifiering begärd"
"De matchar inte"
@@ -44,7 +45,7 @@
"För extra säkerhet vill en annan användare verifiera din identitet. Du kommer att visas en uppsättning emojier att jämföra."
"Du bör se en popup på den andra enheten. Starta verifieringen därifrån nu."
"Starta verifieringen på den andra enheten"
- "Väntar på den andra enheten"
+ "Starta verifieringen på den andra enheten"
"Väntar på den andra användaren"
"När det har accepterats kommer du kunna fortsätta verifieringen."
"Godkänn begäran om att starta verifieringsprocessen på din andra session för att fortsätta."
diff --git a/features/verifysession/impl/src/main/res/values-tr/translations.xml b/features/verifysession/impl/src/main/res/values-tr/translations.xml
index a045731bd4..e718ee2d46 100644
--- a/features/verifysession/impl/src/main/res/values-tr/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-tr/translations.xml
@@ -18,6 +18,7 @@
"Sayıları karşılaştır"
"Yeni oturumunuz artık doğrulandı. Şifrelenmiş mesajlarınıza erişebilir ve diğer kullanıcılar oturumu güvenilir olarak görecektir."
"Artık mesaj gönderirken veya alırken bu kullanıcının kimliğine güvenebilirsiniz."
+ "Cihaz doğrulandı"
"Kurtarma anahtarını girin"
"İstek zaman aşımına uğradı, istek reddedildi veya bir doğrulama uyuşmazlığı vardı."
"Şifrelenmiş mesaj geçmişinize erişmek için siz olduğunuzu kanıtlayın."
@@ -32,7 +33,7 @@
"Doğrulama başarısız"
"Yalnızca bu doğrulamayı siz başlattıysanız devam edin."
"Mesaj geçmişinizi güvende tutmak için diğer cihazı doğrulayın."
- "Artık diğer cihazınızda güvenli bir şekilde mesaj okuyabilir veya gönderebilirsiniz."
+ "Yeni oturumunuz artık doğrulandı. Şifrelenmiş mesajlarınıza erişebilir ve diğer kullanıcılar oturumu güvenilir olarak görecektir."
"Cihaz doğrulandı"
"Doğrulama talep edildi"
"Eşleşmiyorlar"
@@ -44,7 +45,7 @@
"Ekstra güvenlik için, başka bir kullanıcı kimliğinizi doğrulamak istiyor. Karşılaştırmanız için size bir dizi emoji gösterilecektir."
"Diğer cihazda bir açılır pencere görmelisiniz. Doğrulamayı şimdi oradan başlatın."
"Diğer cihazda doğrulamayı başlat"
- "Diğer cihaz bekleniyor"
+ "Diğer cihazda doğrulamayı başlat"
"Diğer kullanıcı bekleniyor"
"Kabul edildikten sonra doğrulama işlemine devam edebileceksiniz."
"Devam etmek için diğer oturumunuzda doğrulama işlemini başlatma isteğini kabul edin."
diff --git a/features/verifysession/impl/src/main/res/values-uk/translations.xml b/features/verifysession/impl/src/main/res/values-uk/translations.xml
index 10c6ae184d..e28304d138 100644
--- a/features/verifysession/impl/src/main/res/values-uk/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-uk/translations.xml
@@ -18,6 +18,7 @@
"Порівняйте цифри"
"Ваш новий сеанс підтверджено. Він матиме доступ до ваших зашифрованих повідомлень, й інші користувачі вважатимуть його надійним."
"Тепер ви можете довіряти особистості цього користувача під час надсилання або отримання повідомлень."
+ "Пристрій перевірено"
"Введіть ключ відновлення"
"Або час очікування запиту минув, або запит було відхилено, або виникла розбіжність у верифікації."
"Доведіть, що це ви, щоб отримати доступ до історії зашифрованих повідомлень."
@@ -32,7 +33,7 @@
"Перевірка не вдалася"
"Продовжуйте, лише якщо ви ініціювали цю перевірку."
"Перевірте інший пристрій, щоб захистити історію повідомлень."
- "Тепер ви можете безпечно читати або надсилати повідомлення на іншому пристрої."
+ "Ваш новий сеанс підтверджено. Він матиме доступ до ваших зашифрованих повідомлень, й інші користувачі вважатимуть його надійним."
"Пристрій перевірено"
"Запитано на верифікацію"
"Вони не збігаються"
@@ -44,7 +45,7 @@
"Для додаткової безпеки інший користувач хоче верифікувати вашу особистість. Вам буде показано набір емоджі для порівняння."
"Ви повинні побачити спливаюче вікно на іншому пристрої. Почніть перевірку звідти."
"Почніть перевірку на іншому пристрої"
- "Очікування іншого пристрою"
+ "Почніть перевірку на іншому пристрої"
"Очікування іншого користувача"
"Після погодження ви зможете продовжити верифікацію."
"Щоб продовжити, прийміть запит на початок процесу верифікації в іншому сеансі."
diff --git a/features/verifysession/impl/src/main/res/values-ur/translations.xml b/features/verifysession/impl/src/main/res/values-ur/translations.xml
index e6f0fcd367..0697e10c00 100644
--- a/features/verifysession/impl/src/main/res/values-ur/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-ur/translations.xml
@@ -16,6 +16,7 @@
"تصدیق کریں کہ نیچے دیے گئے اعداد آپکے دوسرے جلسے میں دکھائے گئے اعداد سے مماثل ہیں۔"
"اعداد کا موازنہ کریں"
"آپ کا نیا جلسہ اب توثیق شدہ ہے۔ اسے آپ کے مرموزکردہ پیغامات تک رسائی حاصل ہے، اور دوسرے صارفین اسے بھروسہ مند کے طور پر دیکھیں گے۔"
+ "آلہ توثیق شدہ"
"بازیابی کلید درج کریں"
"اپنی مرموزکردہ پیغام کی سرگزشت تک رسائی حاصل کرنے کے لیے ثابت کریں کہ یہ آپ ہی ہیں۔"
"ایک موجودہ جلسہ کھولیں"
@@ -24,6 +25,7 @@
"ملانے کا انتظار"
"رموز تعبیری کے منفرد مجموعہ کا موازنہ کریں۔"
"منفرد ایموجی کا موازنہ کریں، یقینی بناتے ہوئے کہ وہ ایک ہی ترتیب میں دکھائی دیں۔"
+ "آپ کا نیا جلسہ اب توثیق شدہ ہے۔ اسے آپ کے مرموزکردہ پیغامات تک رسائی حاصل ہے، اور دوسرے صارفین اسے بھروسہ مند کے طور پر دیکھیں گے۔"
"آلہ توثیق شدہ"
"وہ مماثل نہیں ہیں"
"وہ مماثل ہیں"
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 defdff1102..5204e759b3 100644
--- a/features/verifysession/impl/src/main/res/values-uz/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-uz/translations.xml
@@ -16,6 +16,7 @@
"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."
+ "Qurilma tasdiqlandi"
"Tiklash kalitini kiriting"
"So‘rov vaqti tugab qoldi, so‘rov rad etildi yoki tekshiruv mos kelmadi."
"Shifrlangan xabarlar tarixiga kirish uchun shaxsingizni tasdiqlang."
@@ -30,7 +31,7 @@
"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."
+ "Yangi seansingiz tasdiqlandi. U sizning shifrlangan xabarlaringizga kirish huquqiga ega va boshqa foydalanuvchilar uni ishonchli deb bilishadi."
"Qurilma tasdiqlandi"
"Tasdiqlash talab qilindi"
"Ular mos kelmaydi"
diff --git a/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml b/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml
index 46e26a2e70..7bba014ada 100644
--- a/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml
@@ -18,6 +18,7 @@
"比較數字"
"新的工作階段已完成驗證。它能夠存取您的加密訊息,而其他使用者會將它視為可信任的。"
"現在,您可以在傳送或接收訊息時信任此使用者的身份。"
+ "裝置已驗證"
"輸入復原金鑰"
"請求逾時、請求被拒或是驗證不符。"
"為了存取被加密的歷史訊息,您需要證明這是您本人。"
@@ -32,7 +33,7 @@
"驗證失敗。"
"僅當您啟動此驗證時才繼續。"
"驗證其他裝置以保護您的訊息歷史紀錄安全。"
- "現在,您可以在其他裝置上安全地閱讀或傳送訊息。"
+ "新的工作階段已完成驗證。它能夠存取您的加密訊息,而其他使用者會將它視為可信任的。"
"裝置已驗證"
"已請求驗證"
"不一樣"
@@ -44,7 +45,7 @@
"為了提昇安全性,另一個使用者希望驗證您的身份。您將會看到一組表情符號以進行比較。"
"您應該會在其他裝置上看到一個彈出式視窗。立刻從那裡開始驗證。"
"在其他裝置上開始驗證"
- "正在等待其他裝置"
+ "在其他裝置上開始驗證"
"正在等帶齊他使用者"
"接受後,您就可以繼續進行驗證。"
"準備開始驗證,請到您的其他工作階段接受請求。"
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 113cda4a01..92641d1773 100644
--- a/features/verifysession/impl/src/main/res/values-zh/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-zh/translations.xml
@@ -18,6 +18,7 @@
"比较数字"
"新设备已经成功验证。现在新设备可以访问加密信息,其他用户也会信任这个设备。"
"现在您可以在发送或接收消息时信任该用户的身份。"
+ "设备已验证"
"输入恢复密钥"
"要么请求超时,要么请求被拒绝,要么验证不匹配。"
"证明自己的身份以访问加密历史消息。"
@@ -32,7 +33,7 @@
"验证失败"
"仅在你发起此验证后才继续。"
"验证另一台设备以确保您的消息历史记录保密。"
- "现在,您可以在其他设备上安全地阅读或发送消息。"
+ "新设备已经成功验证。现在新设备可以访问加密信息,其他用户也会信任这个设备。"
"设备已验证"
"已请求验证"
"不匹配"
@@ -44,7 +45,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/IncomingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt
index 06c4956ea9..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
@@ -293,13 +294,14 @@ class IncomingVerificationPresenterTest {
private val anIncomingSessionVerificationRequest = VerificationRequest.Incoming.OtherSession(
details = SessionVerificationRequestDetails(
- senderProfile = SessionVerificationRequestDetails.SenderProfile(
+ senderProfile = MatrixUser(
userId = A_USER_ID,
- displayName = "a device name",
+ displayName = "a user name",
avatarUrl = null,
),
flowId = FlowId("flowId"),
deviceId = A_DEVICE_ID,
+ deviceDisplayName = "a device name",
firstSeenTimestamp = A_TIMESTAMP,
)
)
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/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 140a6258e2..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
@@ -15,13 +15,13 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class ViewFileNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 e22ae371cb..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
@@ -16,13 +16,13 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+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
-@Inject
+@AssistedInject
class ViewFilePresenter(
@Assisted("path") val path: String,
@Assisted("name") val name: String,
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 fce49faffb..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
@@ -15,14 +15,14 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
-import dev.zacsweers.metro.Inject
+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
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class ViewFolderNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
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 8e392855d8..0c5ab61705 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
@@ -15,14 +15,15 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
-import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.AssistedInject
import io.element.android.features.viewfolder.impl.model.Item
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
+import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
-@Inject
+@AssistedInject
class ViewFolderPresenter(
@Assisted val canGoUp: Boolean,
@Assisted val path: String,
@@ -36,7 +37,7 @@ class ViewFolderPresenter(
@Composable
override fun present(): ViewFolderState {
- var content by remember { mutableStateOf(persistentListOf- ()) }
+ var content by remember { mutableStateOf>(persistentListOf()) }
val title = remember {
buildString {
if (path.contains(buildMeta.applicationId)) {
@@ -49,7 +50,7 @@ class ViewFolderPresenter(
content = buildList {
if (canGoUp) add(Item.Parent)
addAll(folderExplorer.getItems(path))
- }.toPersistentList()
+ }.toImmutableList()
}
return ViewFolderState(
title = title,
diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/root/ViewFolderFlowNode.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/root/ViewFolderFlowNode.kt
index 2eb6712364..d57824f2fd 100644
--- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/root/ViewFolderFlowNode.kt
+++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/root/ViewFolderFlowNode.kt
@@ -19,7 +19,7 @@ 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.Inject
+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
@@ -33,7 +33,7 @@ import io.element.android.libraries.architecture.inputs
import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class)
-@Inject
+@AssistedInject
class ViewFolderFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
diff --git a/gradle.properties b/gradle.properties
index 18399ccf06..a2c7228ed2 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -54,3 +54,7 @@ ksp.allow.all.target.configuration=false
# Used to prevent detekt from reusing invalid cached rules
detekt.use.worker.api=true
+
+# Let test include roborazzi verification
+# https://github.com/takahirom/roborazzi?tab=readme-ov-file#roborazzitest
+roborazzi.test.verify=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ea78e146b4..0d4cb342dc 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -6,9 +6,9 @@
# 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
@@ -32,6 +32,7 @@ accompanist = "0.37.3"
# Test
test_core = "1.7.0"
+roborazzi = "1.50.0"
# Jetbrain
datetime = "0.7.1"
@@ -44,14 +45,14 @@ showkase = "1.0.5"
appyx = "1.7.1"
sqldelight = "2.1.0"
wysiwyg = "2.39.0"
-telephoto = "0.17.0"
+telephoto = "0.18.0"
haze = "1.6.10"
# Dependency analysis
dependencyAnalysis = "3.0.4"
# DI
-metro = "0.6.4"
+metro = "0.6.8"
# Auto service
autoservice = "1.1.1"
@@ -149,7 +150,7 @@ test_corektx = { module = "androidx.test:core-ktx", version.ref = "test_core" }
test_arch_core = "androidx.arch.core:core-testing:2.2.0"
test_junit = "junit:junit:4.13.2"
test_runner = "androidx.test:runner:1.7.0"
-test_mockk = "io.mockk:mockk:1.14.5"
+test_mockk = "io.mockk:mockk:1.14.6"
test_konsist = "com.lemonappdev:konsist:0.17.3"
test_turbine = "app.cash.turbine:turbine:1.2.1"
test_truth = "com.google.truth:truth:1.4.5"
@@ -166,7 +167,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
# All new features should not be implemented in the pull request that upgrades the version, developers should
# only fix API breaks and may add some TODOs.
-matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.9.23"
+matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.10.7"
# Others
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
@@ -174,7 +175,6 @@ coil_network_okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version
coil_compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil_gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" }
coil_test = { module = "io.coil-kt.coil3:coil-test", version.ref = "coil" }
-compound = { module = "io.element.android:compound-android", version = "25.7.4" }
datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" }
kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0"
@@ -182,7 +182,7 @@ showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" }
showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" }
jsoup = "org.jsoup:jsoup:1.21.2"
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_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
@@ -190,13 +190,13 @@ sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", ver
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.6.0"
+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.5"
+maplibre = "org.maplibre.gl:android-sdk:12.0.0"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
opusencoder = "io.element.android:opusencoder:1.2.0"
@@ -205,8 +205,8 @@ 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.21.2"
-sentry = "io.sentry:sentry-android:8.22.0"
+posthog = "com.posthog:posthog-android:3.23.0"
+sentry = "io.sentry:sentry-android:8.23.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"
@@ -215,11 +215,10 @@ 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"
metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" }
# Element Call
-element_call_embedded = "io.element.android:element-call-embedded:0.16.0-rc.4"
+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" }
@@ -228,6 +227,11 @@ google_autoservice_annotations = { module = "com.google.auto.service:auto-servic
# Miscellaneous
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
+# Test
+test_roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi" }
+test_roborazzi_compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose", version.ref = "roborazzi" }
+test_roborazzi_junit = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi" }
+
[bundles]
[plugins]
@@ -237,13 +241,15 @@ 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" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+# 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.1.0"
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
-dependencycheck = "org.owasp.dependencycheck:12.1.5"
+dependencycheck = "org.owasp.dependencycheck:12.1.6"
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }
paparazzi = "app.cash.paparazzi:2.0.0-alpha02"
+roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" }
knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" }
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..f69708cc9d
--- /dev/null
+++ b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectPresenter.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.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.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
+
+@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,
+ )
+ }
+ .toImmutableList()
+ }
+
+ 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..5d99bf3b0c
--- /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.toImmutableList
+
+open class AccountSelectStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ anAccountSelectState(),
+ anAccountSelectState(accounts = aMatrixUserList()),
+ )
+}
+
+private fun anAccountSelectState(
+ accounts: List = listOf(),
+) = AccountSelectState(
+ accounts = accounts.toImmutableList(),
+)
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/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/DelegateTransitionHandler.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/DelegateTransitionHandler.kt
new file mode 100644
index 0000000000..642ff6fc3a
--- /dev/null
+++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/DelegateTransitionHandler.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.architecture.appyx
+
+import android.annotation.SuppressLint
+import androidx.compose.animation.core.Transition
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.navigation.transition.ModifierTransitionHandler
+import com.bumble.appyx.core.navigation.transition.TransitionDescriptor
+
+/**
+ * A [ModifierTransitionHandler] that delegates the creation of the modifier to another handler
+ * based on the [NavTarget]. The idea is to allow different transitions for different [NavTarget]s.
+ */
+class DelegateTransitionHandler(
+ private val handlerProvider: (NavTarget) -> ModifierTransitionHandler,
+) : ModifierTransitionHandler() {
+ @SuppressLint("ModifierFactoryExtensionFunction")
+ override fun createModifier(modifier: Modifier, transition: Transition, descriptor: TransitionDescriptor): Modifier {
+ return handlerProvider(descriptor.element).createModifier(modifier, transition, descriptor)
+ }
+}
+
+@Composable
+fun rememberDelegateTransitionHandler(
+ handlerProvider: (NavTarget) -> ModifierTransitionHandler,
+): ModifierTransitionHandler =
+ remember(handlerProvider) { DelegateTransitionHandler(handlerProvider = handlerProvider) }
diff --git a/libraries/compound/build.gradle.kts b/libraries/compound/build.gradle.kts
new file mode 100644
index 0000000000..cbdb09d451
--- /dev/null
+++ b/libraries/compound/build.gradle.kts
@@ -0,0 +1,29 @@
+import extension.testCommonDependencies
+
+/*
+ * Copyright 2022, 2025 New 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")
+ alias(libs.plugins.roborazzi)
+}
+
+android {
+ namespace = "io.element.android.compound"
+
+ testOptions {
+ unitTests.isIncludeAndroidResources = true
+ }
+
+ dependencies {
+ implementation(libs.showkase)
+ testCommonDependencies(libs)
+ testImplementation(libs.test.roborazzi)
+ testImplementation(libs.test.roborazzi.compose)
+ testImplementation(libs.test.roborazzi.junit)
+ }
+}
diff --git a/libraries/compound/screenshots/Avatar Colors - Dark.png b/libraries/compound/screenshots/Avatar Colors - Dark.png
new file mode 100644
index 0000000000..5cf6cbda89
--- /dev/null
+++ b/libraries/compound/screenshots/Avatar Colors - Dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:caf2de32caf0fa5368da899297e2eabd5a0c6891dd94f81295ef8a933d79ce16
+size 10751
diff --git a/libraries/compound/screenshots/Avatar Colors - Light.png b/libraries/compound/screenshots/Avatar Colors - Light.png
new file mode 100644
index 0000000000..7069762082
--- /dev/null
+++ b/libraries/compound/screenshots/Avatar Colors - Light.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:80e44e94d7b23af2ec4fd1c5a871851ae2567b40e478b30145de199076f20e95
+size 11296
diff --git a/libraries/compound/screenshots/Compound Icons - Dark.png b/libraries/compound/screenshots/Compound Icons - Dark.png
new file mode 100644
index 0000000000..acac494f89
--- /dev/null
+++ b/libraries/compound/screenshots/Compound Icons - Dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6bc48b9f792da838f9fc2c2a630cbbbb906de851a138cc1ac8b7bf67b801ad84
+size 211300
diff --git a/libraries/compound/screenshots/Compound Icons - Light.png b/libraries/compound/screenshots/Compound Icons - Light.png
new file mode 100644
index 0000000000..c6b76c46bb
--- /dev/null
+++ b/libraries/compound/screenshots/Compound Icons - Light.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e85fd2c67ff42829b8580ab5c0c2af1fee40973f5c0b34ef7de00e3663cee8e4
+size 223041
diff --git a/libraries/compound/screenshots/Compound Icons - Rtl.png b/libraries/compound/screenshots/Compound Icons - Rtl.png
new file mode 100644
index 0000000000..ce2ffe2e88
--- /dev/null
+++ b/libraries/compound/screenshots/Compound Icons - Rtl.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:406b62991171a146e891f95bcad0321ebc60cf1fe2cabc9caedbb17fb062af13
+size 224320
diff --git a/libraries/compound/screenshots/Compound Semantic Colors - Dark HC.png b/libraries/compound/screenshots/Compound Semantic Colors - Dark HC.png
new file mode 100644
index 0000000000..4890b91886
--- /dev/null
+++ b/libraries/compound/screenshots/Compound Semantic Colors - Dark HC.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:578e9b5a38791e2686a7b9ba5c461eb1d1fb29dfbe950bf46c113ad75ceac175
+size 327758
diff --git a/libraries/compound/screenshots/Compound Semantic Colors - Dark.png b/libraries/compound/screenshots/Compound Semantic Colors - Dark.png
new file mode 100644
index 0000000000..4cc125b4c8
--- /dev/null
+++ b/libraries/compound/screenshots/Compound Semantic Colors - Dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4cab40fc0506c8f2a2efafb1199e85f1da3ebacb49b176e9105e3f95175f85ee
+size 325565
diff --git a/libraries/compound/screenshots/Compound Semantic Colors - Light HC.png b/libraries/compound/screenshots/Compound Semantic Colors - Light HC.png
new file mode 100644
index 0000000000..5a8f5a6b32
--- /dev/null
+++ b/libraries/compound/screenshots/Compound Semantic Colors - Light HC.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:174f9d4ee70a29c0c8c2a01a15daeb14281530678ff7d7fb19a208bfd789533a
+size 309210
diff --git a/libraries/compound/screenshots/Compound Semantic Colors - Light.png b/libraries/compound/screenshots/Compound Semantic Colors - Light.png
new file mode 100644
index 0000000000..f010626dda
--- /dev/null
+++ b/libraries/compound/screenshots/Compound Semantic Colors - Light.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7598b98462c015f2bf74b3ea3ad95fc0220b2efb9bb81ac56025cf6a158e3f8a
+size 308976
diff --git a/libraries/compound/screenshots/Compound Typography.png b/libraries/compound/screenshots/Compound Typography.png
new file mode 100644
index 0000000000..095ad6c71d
--- /dev/null
+++ b/libraries/compound/screenshots/Compound Typography.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ac84a7175c4a4897aa28eddcf722b7997c6576f612eb38fa09ffabcf7be11e00
+size 119496
diff --git a/libraries/compound/screenshots/Compound Vector Icons - Dark.png b/libraries/compound/screenshots/Compound Vector Icons - Dark.png
new file mode 100644
index 0000000000..2b535c348b
--- /dev/null
+++ b/libraries/compound/screenshots/Compound Vector Icons - Dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6ff6dfdfab51332cad3bdfa351a4d0e305de5f899853575a8514858cc871e904
+size 83609
diff --git a/libraries/compound/screenshots/Compound Vector Icons - Light.png b/libraries/compound/screenshots/Compound Vector Icons - Light.png
new file mode 100644
index 0000000000..bc29dcd24d
--- /dev/null
+++ b/libraries/compound/screenshots/Compound Vector Icons - Light.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ca38f5f23c282a6dc4c01a54705f71bf8c927aeac9c1df0a1f3abe50c10b1b85
+size 89336
diff --git a/libraries/compound/screenshots/ForcedDarkElementTheme.png b/libraries/compound/screenshots/ForcedDarkElementTheme.png
new file mode 100644
index 0000000000..d7182aa47c
--- /dev/null
+++ b/libraries/compound/screenshots/ForcedDarkElementTheme.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:03bdb2f3de01d40b8f85ba87b395cdab2e5225aded2f40a0892077798bca6066
+size 22328
diff --git a/libraries/compound/screenshots/Legacy Colors.png b/libraries/compound/screenshots/Legacy Colors.png
new file mode 100644
index 0000000000..2ecdda2ed7
--- /dev/null
+++ b/libraries/compound/screenshots/Legacy Colors.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3e9d676a1ef20a7228e985f62d346265ff9c31d1860a219540f012c063c9345e
+size 33652
diff --git a/libraries/compound/screenshots/Material Typography.png b/libraries/compound/screenshots/Material Typography.png
new file mode 100644
index 0000000000..6c22c364b9
--- /dev/null
+++ b/libraries/compound/screenshots/Material Typography.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d27f813acc9e8dc3960f5205809c8c0d2ba0fa51fd9e3ca07964b866b125e87d
+size 110171
diff --git a/libraries/compound/screenshots/Material3 Colors - Dark HC.png b/libraries/compound/screenshots/Material3 Colors - Dark HC.png
new file mode 100644
index 0000000000..4cc7609c89
--- /dev/null
+++ b/libraries/compound/screenshots/Material3 Colors - Dark HC.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:85969829577e158bdd1d0f21c8b3a2334dcde79cb50d5e2331d06d5423332be2
+size 160754
diff --git a/libraries/compound/screenshots/Material3 Colors - Dark.png b/libraries/compound/screenshots/Material3 Colors - Dark.png
new file mode 100644
index 0000000000..3738cf6a79
--- /dev/null
+++ b/libraries/compound/screenshots/Material3 Colors - Dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ca64da1dc373dc49503ee525ae3331e73d0ab12053993a1ef19dcab1e67b08c4
+size 159123
diff --git a/libraries/compound/screenshots/Material3 Colors - Light HC.png b/libraries/compound/screenshots/Material3 Colors - Light HC.png
new file mode 100644
index 0000000000..8d64857d13
--- /dev/null
+++ b/libraries/compound/screenshots/Material3 Colors - Light HC.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:01622bca20a132ec5a874fc1a2d0ffd45e7ce6d7849c4d607d79c7bc51d6c6a9
+size 163322
diff --git a/libraries/compound/screenshots/Material3 Colors - Light.png b/libraries/compound/screenshots/Material3 Colors - Light.png
new file mode 100644
index 0000000000..a1d5d1f2ce
--- /dev/null
+++ b/libraries/compound/screenshots/Material3 Colors - Light.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:87c0c4ff42d17137d554708ce33f40f214ec608eca4ca87af0b2adab63de6bb7
+size 162891
diff --git a/libraries/compound/screenshots/MaterialText Colors.png b/libraries/compound/screenshots/MaterialText Colors.png
new file mode 100644
index 0000000000..f8f77ccca2
--- /dev/null
+++ b/libraries/compound/screenshots/MaterialText Colors.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4be10c3bb9900d27a3b406eca0cb902b0ff9cdf90e8e3cf1ae7760aa7c5d47d9
+size 377446
diff --git a/libraries/compound/screenshots/MaterialYou Theme - Dark.png b/libraries/compound/screenshots/MaterialYou Theme - Dark.png
new file mode 100644
index 0000000000..8c5bc9d1ef
--- /dev/null
+++ b/libraries/compound/screenshots/MaterialYou Theme - Dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c166e5371bb1922a9c016438a3cdfd0d68197237969d53a04f92baf6d53c4ac0
+size 164925
diff --git a/libraries/compound/screenshots/MaterialYou Theme - Light.png b/libraries/compound/screenshots/MaterialYou Theme - Light.png
new file mode 100644
index 0000000000..70cccac69b
--- /dev/null
+++ b/libraries/compound/screenshots/MaterialYou Theme - Light.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d936948cbad69d6935f2d2738d33682a55f044dfb1af8b5c9b8323c5f4318971
+size 163558
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/annotations/CoreColorToken.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/annotations/CoreColorToken.kt
new file mode 100644
index 0000000000..a56ecb38a8
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/annotations/CoreColorToken.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.annotations
+
+@RequiresOptIn(
+ message = "This is a Core color token, which should only be used to declare semantic colors, otherwise it" +
+ " would look the same on both light and dark modes. Only use it as is if you know what you are doing."
+)
+@Retention(AnnotationRetention.BINARY)
+@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
+annotation class CoreColorToken
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/previews/ColorListPreview.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/ColorListPreview.kt
new file mode 100644
index 0000000000..715da1ebc3
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/ColorListPreview.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.compound.previews
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import kotlinx.collections.immutable.ImmutableMap
+import kotlin.math.ceil
+
+@Composable
+fun ColorListPreview(
+ backgroundColor: Color,
+ foregroundColor: Color,
+ colors: ImmutableMap,
+ modifier: Modifier = Modifier,
+ numColumns: Int = 1,
+) {
+ Row(
+ modifier = modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ colors.keys
+ .chunked(ceil(colors.keys.size / numColumns.toDouble()).toInt())
+ .forEach { subList ->
+ Column(
+ modifier = Modifier
+ .background(color = backgroundColor)
+ .weight(1f)
+ ) {
+ subList.forEach { name ->
+ val color = colors[name]!!
+ ColorPreview(
+ backgroundColor = backgroundColor,
+ foregroundColor = foregroundColor,
+ name = name,
+ color = color
+ )
+ }
+ Spacer(modifier = Modifier.height(2.dp))
+ }
+ }
+ }
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/previews/ColorPreview.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/ColorPreview.kt
new file mode 100644
index 0000000000..3248adeedf
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/ColorPreview.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.compound.previews
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import io.element.android.compound.utils.toHrf
+
+@Composable
+fun ColorPreview(
+ backgroundColor: Color,
+ foregroundColor: Color,
+ name: String,
+ color: Color,
+ modifier: Modifier = Modifier,
+) {
+ Column(modifier = modifier.fillMaxWidth()) {
+ Text(
+ modifier = Modifier.padding(horizontal = 10.dp),
+ text = name + " " + color.toHrf(),
+ fontSize = 6.sp,
+ color = foregroundColor,
+ )
+ val backgroundBrush = Brush.linearGradient(
+ listOf(
+ backgroundColor,
+ foregroundColor,
+ )
+ )
+ Row(
+ modifier = Modifier.background(backgroundBrush)
+ ) {
+ repeat(2) {
+ Box(
+ modifier = Modifier
+ .padding(1.dp)
+ .background(Color.White)
+ .background(color = color)
+ .height(10.dp)
+ .weight(1f)
+ )
+ }
+ }
+ }
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/previews/ColorsSchemePreview.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/ColorsSchemePreview.kt
new file mode 100644
index 0000000000..cf660ba236
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/ColorsSchemePreview.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.compound.previews
+
+import androidx.compose.material3.ColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import kotlinx.collections.immutable.persistentMapOf
+
+@Composable
+internal fun ColorsSchemePreview(
+ backgroundColor: Color,
+ foregroundColor: Color,
+ colorScheme: ColorScheme,
+ modifier: Modifier = Modifier,
+) {
+ val colors = persistentMapOf(
+ "primary" to colorScheme.primary,
+ "onPrimary" to colorScheme.onPrimary,
+ "primaryContainer" to colorScheme.primaryContainer,
+ "onPrimaryContainer" to colorScheme.onPrimaryContainer,
+ "inversePrimary" to colorScheme.inversePrimary,
+ "secondary" to colorScheme.secondary,
+ "onSecondary" to colorScheme.onSecondary,
+ "secondaryContainer" to colorScheme.secondaryContainer,
+ "onSecondaryContainer" to colorScheme.onSecondaryContainer,
+ "tertiary" to colorScheme.tertiary,
+ "onTertiary" to colorScheme.onTertiary,
+ "tertiaryContainer" to colorScheme.tertiaryContainer,
+ "onTertiaryContainer" to colorScheme.onTertiaryContainer,
+ "background" to colorScheme.background,
+ "onBackground" to colorScheme.onBackground,
+ "surface" to colorScheme.surface,
+ "onSurface" to colorScheme.onSurface,
+ "surfaceVariant" to colorScheme.surfaceVariant,
+ "onSurfaceVariant" to colorScheme.onSurfaceVariant,
+ "surfaceTint" to colorScheme.surfaceTint,
+ "inverseSurface" to colorScheme.inverseSurface,
+ "inverseOnSurface" to colorScheme.inverseOnSurface,
+ "error" to colorScheme.error,
+ "onError" to colorScheme.onError,
+ "errorContainer" to colorScheme.errorContainer,
+ "onErrorContainer" to colorScheme.onErrorContainer,
+ "outline" to colorScheme.outline,
+ "outlineVariant" to colorScheme.outlineVariant,
+ "scrim" to colorScheme.scrim,
+ )
+ ColorListPreview(
+ backgroundColor = backgroundColor,
+ foregroundColor = foregroundColor,
+ colors = colors,
+ modifier = modifier,
+ )
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/previews/CompoundIconsPreview.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/CompoundIconsPreview.kt
new file mode 100644
index 0000000000..0ee0546ca4
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/CompoundIconsPreview.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2023, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.previews
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+
+@Preview(widthDp = 730, heightDp = 1800)
+@Composable
+internal fun IconsCompoundPreviewLight() = ElementTheme {
+ IconsCompoundPreview()
+}
+
+@Preview(widthDp = 730, heightDp = 1800)
+@Composable
+internal fun IconsCompoundPreviewRtl() = ElementTheme {
+ CompositionLocalProvider(
+ LocalLayoutDirection provides LayoutDirection.Rtl,
+ ) {
+ IconsCompoundPreview(
+ title = "Compound Icons Rtl",
+ )
+ }
+}
+
+@Preview(widthDp = 730, heightDp = 1800)
+@Composable
+internal fun IconsCompoundPreviewDark() = ElementTheme(darkTheme = true) {
+ IconsCompoundPreview()
+}
+
+@Composable
+private fun IconsCompoundPreview(
+ title: String = "Compound Icons",
+) {
+ val context = LocalContext.current
+ val content: Sequence<@Composable ColumnScope.() -> Unit> = sequence {
+ for (icon in CompoundIcons.allResIds) {
+ yield {
+ Icon(
+ modifier = Modifier.size(32.dp),
+ imageVector = ImageVector.vectorResource(icon),
+ contentDescription = null,
+ )
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = context.resources.getResourceEntryName(icon)
+ .removePrefix("ic_compound_")
+ .replace("_", " "),
+ textAlign = TextAlign.Center,
+ style = ElementTheme.typography.fontBodyXsMedium,
+ color = ElementTheme.colors.textSecondary,
+ )
+ }
+ }
+ }
+ IconsPreview(
+ title = title,
+ content = content.toImmutableList(),
+ )
+}
+
+@Composable
+internal fun IconsPreview(
+ title: String,
+ content: ImmutableList<@Composable ColumnScope.() -> Unit>,
+) = Surface {
+ Column(
+ modifier = Modifier
+ .background(MaterialTheme.colorScheme.surfaceVariant)
+ .padding(16.dp)
+ .width(IntrinsicSize.Max),
+ verticalArrangement = Arrangement.spacedBy(6.dp),
+ ) {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp),
+ style = ElementTheme.typography.fontHeadingSmMedium,
+ text = title,
+ textAlign = TextAlign.Center,
+ )
+ content.chunked(10).forEach { chunk ->
+ Row(
+ modifier = Modifier.height(IntrinsicSize.Max),
+ // Keep same order of icons for an easier comparison of previews
+ horizontalArrangement = Arrangement.Absolute.Left,
+ ) {
+ chunk.forEachIndexed { index, icon ->
+ Column(
+ modifier = Modifier
+ .background(MaterialTheme.colorScheme.background)
+ .fillMaxHeight()
+ .width(64.dp)
+ .padding(4.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ icon()
+ }
+ if (index < chunk.size - 1) {
+ Spacer(modifier = Modifier.width(6.dp))
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/previews/SemanticColorsPreview.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/SemanticColorsPreview.kt
new file mode 100644
index 0000000000..d641ff39c5
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/SemanticColorsPreview.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.previews
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.compoundColorsHcDark
+import kotlinx.collections.immutable.ImmutableMap
+import kotlinx.collections.immutable.persistentMapOf
+
+@Preview(heightDp = 2000)
+@Composable
+internal fun CompoundSemanticColorsLight() = ElementTheme {
+ Surface {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Text("Compound Semantic Colors - Light")
+ ColorListPreview(
+ backgroundColor = Color.White,
+ foregroundColor = Color.Black,
+ colors = getSemanticColors(),
+ numColumns = 2,
+ )
+ }
+ }
+}
+
+@Preview(heightDp = 2000)
+@Composable
+internal fun CompoundSemanticColorsLightHc() = ElementTheme(
+ compoundDark = compoundColorsHcDark,
+) {
+ Surface {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Text("Compound Semantic Colors - Light HC")
+ ColorListPreview(
+ backgroundColor = Color.White,
+ foregroundColor = Color.Black,
+ colors = getSemanticColors(),
+ numColumns = 2,
+ )
+ }
+ }
+}
+
+@Preview(heightDp = 2000)
+@Composable
+internal fun CompoundSemanticColorsDark() = ElementTheme(darkTheme = true) {
+ Surface {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Text("Compound Semantic Colors - Dark")
+ ColorListPreview(
+ backgroundColor = Color.White,
+ foregroundColor = Color.Black,
+ colors = getSemanticColors(),
+ numColumns = 2,
+ )
+ }
+ }
+}
+
+@Preview(heightDp = 2000)
+@Composable
+internal fun CompoundSemanticColorsDarkHc() = ElementTheme(
+ darkTheme = true,
+ compoundDark = compoundColorsHcDark,
+) {
+ Surface {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Text("Compound Semantic Colors - Dark HC")
+ ColorListPreview(
+ backgroundColor = Color.White,
+ foregroundColor = Color.Black,
+ colors = getSemanticColors(),
+ numColumns = 2,
+ )
+ }
+ }
+}
+
+@Composable
+private fun getSemanticColors(): ImmutableMap {
+ return with(ElementTheme.colors) {
+ persistentMapOf(
+ "bgAccentHovered" to bgAccentHovered,
+ "bgAccentPressed" to bgAccentPressed,
+ "bgAccentRest" to bgAccentRest,
+ "bgAccentSelected" to bgAccentSelected,
+ "bgActionPrimaryDisabled" to bgActionPrimaryDisabled,
+ "bgActionPrimaryHovered" to bgActionPrimaryHovered,
+ "bgActionPrimaryPressed" to bgActionPrimaryPressed,
+ "bgActionPrimaryRest" to bgActionPrimaryRest,
+ "bgActionSecondaryHovered" to bgActionSecondaryHovered,
+ "bgActionSecondaryPressed" to bgActionSecondaryPressed,
+ "bgActionSecondaryRest" to bgActionSecondaryRest,
+ "bgBadgeAccent" to bgBadgeAccent,
+ "bgBadgeDefault" to bgBadgeDefault,
+ "bgBadgeInfo" to bgBadgeInfo,
+ "bgCanvasDefault" to bgCanvasDefault,
+ "bgCanvasDefaultLevel1" to bgCanvasDefaultLevel1,
+ "bgCanvasDisabled" to bgCanvasDisabled,
+ "bgCriticalHovered" to bgCriticalHovered,
+ "bgCriticalPrimary" to bgCriticalPrimary,
+ "bgCriticalSubtle" to bgCriticalSubtle,
+ "bgCriticalSubtleHovered" to bgCriticalSubtleHovered,
+ "bgDecorative1" to bgDecorative1,
+ "bgDecorative2" to bgDecorative2,
+ "bgDecorative3" to bgDecorative3,
+ "bgDecorative4" to bgDecorative4,
+ "bgDecorative5" to bgDecorative5,
+ "bgDecorative6" to bgDecorative6,
+ "bgInfoSubtle" to bgInfoSubtle,
+ "bgSubtlePrimary" to bgSubtlePrimary,
+ "bgSubtleSecondary" to bgSubtleSecondary,
+ "bgSubtleSecondaryLevel0" to bgSubtleSecondaryLevel0,
+ "bgSuccessSubtle" to bgSuccessSubtle,
+ "borderAccentSubtle" to borderAccentSubtle,
+ "borderCriticalHovered" to borderCriticalHovered,
+ "borderCriticalPrimary" to borderCriticalPrimary,
+ "borderCriticalSubtle" to borderCriticalSubtle,
+ "borderDisabled" to borderDisabled,
+ "borderFocused" to borderFocused,
+ "borderInfoSubtle" to borderInfoSubtle,
+ "borderInteractiveHovered" to borderInteractiveHovered,
+ "borderInteractivePrimary" to borderInteractivePrimary,
+ "borderInteractiveSecondary" to borderInteractiveSecondary,
+ "borderSuccessSubtle" to borderSuccessSubtle,
+ "gradientActionStop1" to gradientActionStop1,
+ "gradientActionStop2" to gradientActionStop2,
+ "gradientActionStop3" to gradientActionStop3,
+ "gradientActionStop4" to gradientActionStop4,
+ "gradientInfoStop1" to gradientInfoStop1,
+ "gradientInfoStop2" to gradientInfoStop2,
+ "gradientInfoStop3" to gradientInfoStop3,
+ "gradientInfoStop4" to gradientInfoStop4,
+ "gradientInfoStop5" to gradientInfoStop5,
+ "gradientInfoStop6" to gradientInfoStop6,
+ "gradientSubtleStop1" to gradientSubtleStop1,
+ "gradientSubtleStop2" to gradientSubtleStop2,
+ "gradientSubtleStop3" to gradientSubtleStop3,
+ "gradientSubtleStop4" to gradientSubtleStop4,
+ "gradientSubtleStop5" to gradientSubtleStop5,
+ "gradientSubtleStop6" to gradientSubtleStop6,
+ "iconAccentPrimary" to iconAccentPrimary,
+ "iconAccentTertiary" to iconAccentTertiary,
+ "iconCriticalPrimary" to iconCriticalPrimary,
+ "iconDisabled" to iconDisabled,
+ "iconInfoPrimary" to iconInfoPrimary,
+ "iconOnSolidPrimary" to iconOnSolidPrimary,
+ "iconPrimary" to iconPrimary,
+ "iconPrimaryAlpha" to iconPrimaryAlpha,
+ "iconQuaternary" to iconQuaternary,
+ "iconQuaternaryAlpha" to iconQuaternaryAlpha,
+ "iconSecondary" to iconSecondary,
+ "iconSecondaryAlpha" to iconSecondaryAlpha,
+ "iconSuccessPrimary" to iconSuccessPrimary,
+ "iconTertiary" to iconTertiary,
+ "iconTertiaryAlpha" to iconTertiaryAlpha,
+ "textActionAccent" to textActionAccent,
+ "textActionPrimary" to textActionPrimary,
+ "textBadgeAccent" to textBadgeAccent,
+ "textBadgeInfo" to textBadgeInfo,
+ "textCriticalPrimary" to textCriticalPrimary,
+ "textDecorative1" to textDecorative1,
+ "textDecorative2" to textDecorative2,
+ "textDecorative3" to textDecorative3,
+ "textDecorative4" to textDecorative4,
+ "textDecorative5" to textDecorative5,
+ "textDecorative6" to textDecorative6,
+ "textDisabled" to textDisabled,
+ "textInfoPrimary" to textInfoPrimary,
+ "textLinkExternal" to textLinkExternal,
+ "textOnSolidPrimary" to textOnSolidPrimary,
+ "textPrimary" to textPrimary,
+ "textSecondary" to textSecondary,
+ "textSuccessPrimary" to textSuccessPrimary,
+ "isLight" to if (isLight) Color.White else Color.Black,
+ )
+ }
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/previews/Typography.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/Typography.kt
new file mode 100644
index 0000000000..5d5f31f203
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/Typography.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.compound.previews
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+
+@Preview
+@Composable
+internal fun TypographyPreview() = ElementTheme {
+ Surface {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ with(ElementTheme.materialTypography) {
+ TypographyTokenPreview(displayLarge, "Display large")
+ TypographyTokenPreview(displayMedium, "Display medium")
+ TypographyTokenPreview(displaySmall, "Display small")
+ TypographyTokenPreview(headlineLarge, "Headline large")
+ TypographyTokenPreview(headlineMedium, "Headline medium")
+ TypographyTokenPreview(headlineSmall, "Headline small")
+ TypographyTokenPreview(titleLarge, "Title large")
+ TypographyTokenPreview(titleMedium, "Title medium")
+ TypographyTokenPreview(titleSmall, "Title small")
+ TypographyTokenPreview(bodyLarge, "Body large")
+ TypographyTokenPreview(bodyMedium, "Body medium")
+ TypographyTokenPreview(bodySmall, "Body small")
+ TypographyTokenPreview(labelLarge, "Label large")
+ TypographyTokenPreview(labelMedium, "Label medium")
+ TypographyTokenPreview(labelSmall, "Label small")
+ }
+ }
+ }
+}
+
+@Composable
+private fun TypographyTokenPreview(style: TextStyle, text: String) {
+ Text(text = text, style = style)
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/showkase/CompoundShowkaseRootModule.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/showkase/CompoundShowkaseRootModule.kt
new file mode 100644
index 0000000000..f1c7b3f8d7
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/showkase/CompoundShowkaseRootModule.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.showkase
+
+import com.airbnb.android.showkase.annotation.ShowkaseRoot
+import com.airbnb.android.showkase.annotation.ShowkaseRootModule
+
+@ShowkaseRoot
+class CompoundShowkaseRootModule : ShowkaseRootModule
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/AvatarColors.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/AvatarColors.kt
new file mode 100644
index 0000000000..506e1c8063
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/AvatarColors.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2023, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.theme
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+
+/**
+ * Data class to hold avatar colors.
+ */
+data class AvatarColors(
+ /** Background color for the avatar. */
+ val background: Color,
+ /** Foreground color for the avatar. */
+ val foreground: Color,
+)
+
+/**
+ * Avatar colors using semantic tokens.
+ */
+@Composable
+fun avatarColors(): List {
+ return listOf(
+ AvatarColors(background = ElementTheme.colors.bgDecorative1, foreground = ElementTheme.colors.textDecorative1),
+ AvatarColors(background = ElementTheme.colors.bgDecorative2, foreground = ElementTheme.colors.textDecorative2),
+ AvatarColors(background = ElementTheme.colors.bgDecorative3, foreground = ElementTheme.colors.textDecorative3),
+ AvatarColors(background = ElementTheme.colors.bgDecorative4, foreground = ElementTheme.colors.textDecorative4),
+ AvatarColors(background = ElementTheme.colors.bgDecorative5, foreground = ElementTheme.colors.textDecorative5),
+ AvatarColors(background = ElementTheme.colors.bgDecorative6, foreground = ElementTheme.colors.textDecorative6),
+ )
+}
+
+@Preview
+@Composable
+internal fun AvatarColorsPreviewLight() {
+ ElementTheme {
+ val chunks = avatarColors().chunked(4)
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ for (chunk in chunks) {
+ AvatarColorRow(chunk.toImmutableList())
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+internal fun AvatarColorsPreviewDark() {
+ ElementTheme(darkTheme = true) {
+ val chunks = avatarColors().chunked(4)
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ for (chunk in chunks) {
+ AvatarColorRow(chunk.toImmutableList())
+ }
+ }
+ }
+}
+
+@Composable
+private fun AvatarColorRow(colors: ImmutableList) {
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ colors.forEach { color ->
+ Box(
+ modifier = Modifier.size(48.dp)
+ .background(color.background),
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = "A",
+ color = color.foreground,
+ )
+ }
+ }
+ }
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ElementTheme.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ElementTheme.kt
new file mode 100644
index 0000000000..7535de0acd
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ElementTheme.kt
@@ -0,0 +1,167 @@
+/*
+ * 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.compound.theme
+
+import android.os.Build
+import androidx.activity.ComponentActivity
+import androidx.activity.SystemBarStyle
+import androidx.activity.compose.LocalActivity
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Typography
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import io.element.android.compound.tokens.compoundTypography
+import io.element.android.compound.tokens.generated.SemanticColors
+import io.element.android.compound.tokens.generated.TypographyTokens
+import io.element.android.compound.tokens.generated.compoundColorsDark
+import io.element.android.compound.tokens.generated.compoundColorsLight
+
+/**
+ * Inspired from https://medium.com/@lucasyujideveloper/54cbcbde1ace
+ */
+object ElementTheme {
+ /**
+ * The current [SemanticColors] provided by [ElementTheme].
+ * These come from Compound and are the recommended colors to use for custom components.
+ * In Figma, these colors usually have the `Light/` or `Dark/` prefix.
+ */
+ val colors: SemanticColors
+ @Composable
+ @ReadOnlyComposable
+ get() = LocalCompoundColors.current
+
+ /**
+ * The current Material 3 [ColorScheme] provided by [ElementTheme], coming from [MaterialTheme].
+ * In Figma, these colors usually have the `M3/` prefix.
+ */
+ val materialColors: ColorScheme
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.colorScheme
+
+ /**
+ * Compound [Typography] tokens. In Figma, these have the `Android/font/` prefix.
+ */
+ val typography: TypographyTokens = TypographyTokens
+
+ /**
+ * Material 3 [Typography] tokens. In Figma, these have the `M3 Typography/` prefix.
+ */
+ val materialTypography: Typography
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.typography
+
+ /**
+ * Returns whether the theme version used is the light or the dark one.
+ */
+ val isLightTheme: Boolean
+ @Composable
+ @ReadOnlyComposable
+ get() = LocalCompoundColors.current.isLight
+}
+
+// Global variables (application level)
+internal val LocalCompoundColors = staticCompositionLocalOf { compoundColorsLight }
+
+/**
+ * Sets up the theme for the application, or a part of it.
+ *
+ * @param darkTheme whether to use the dark theme or not. If `true`, the dark theme will be used.
+ * @param applySystemBarsUpdate whether to update the system bars color scheme or not when the theme changes. It's `true` by default.
+ * This is specially useful when you want to apply an alternate theme to a part of the app but don't want it to affect the system bars.
+ * @param lightStatusBar whether to use a light status bar color scheme or not. By default, it's the opposite of [darkTheme].
+ * @param dynamicColor whether to enable MaterialYou or not. It's `false` by default.
+ * @param compoundLight the [SemanticColors] to use in light theme.
+ * @param compoundDark the [SemanticColors] to use in dark theme.
+ * @param materialColorsLight the Material 3 [ColorScheme] to use in light theme.
+ * @param materialColorsDark the Material 3 [ColorScheme] to use in dark theme.
+ * @param typography the Material 3 [Typography] tokens to use. It'll use [compoundTypography] by default.
+ * @param content the content to apply the theme to.
+ */
+@Composable
+fun ElementTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ applySystemBarsUpdate: Boolean = true,
+ lightStatusBar: Boolean = !darkTheme,
+ // true to enable MaterialYou
+ dynamicColor: Boolean = false,
+ compoundLight: SemanticColors = compoundColorsLight,
+ compoundDark: SemanticColors = compoundColorsDark,
+ materialColorsLight: ColorScheme = compoundLight.toMaterialColorScheme(),
+ materialColorsDark: ColorScheme = compoundDark.toMaterialColorScheme(),
+ typography: Typography = compoundTypography,
+ content: @Composable () -> Unit,
+) {
+ val currentCompoundColor = when {
+ darkTheme -> compoundDark
+ else -> compoundLight
+ }
+
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+ darkTheme -> materialColorsDark
+ else -> materialColorsLight
+ }
+
+ val statusBarColorScheme = if (dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val context = LocalContext.current
+ if (lightStatusBar) {
+ dynamicDarkColorScheme(context)
+ } else {
+ dynamicLightColorScheme(context)
+ }
+ } else {
+ colorScheme
+ }
+
+ if (applySystemBarsUpdate) {
+ val activity = LocalActivity.current as? ComponentActivity
+ LaunchedEffect(statusBarColorScheme, darkTheme, lightStatusBar) {
+ activity?.enableEdgeToEdge(
+ // For Status bar use the background color of the app
+ statusBarStyle = SystemBarStyle.auto(
+ lightScrim = statusBarColorScheme.background.toArgb(),
+ darkScrim = statusBarColorScheme.background.toArgb(),
+ detectDarkMode = { !lightStatusBar }
+ ),
+ // For Navigation bar use a transparent color so the content can be seen through it
+ navigationBarStyle = if (darkTheme) {
+ SystemBarStyle.dark(Color.Transparent.toArgb())
+ } else {
+ SystemBarStyle.light(Color.Transparent.toArgb(), Color.Transparent.toArgb())
+ }
+ )
+ }
+ }
+ CompositionLocalProvider(
+ LocalCompoundColors provides currentCompoundColor,
+ LocalContentColor provides colorScheme.onSurface,
+ ) {
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = typography,
+ content = content
+ )
+ }
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ForcedDarkElementTheme.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ForcedDarkElementTheme.kt
new file mode 100644
index 0000000000..cd168713ae
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ForcedDarkElementTheme.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.compound.theme
+
+import androidx.activity.ComponentActivity
+import androidx.activity.SystemBarStyle
+import androidx.activity.compose.LocalActivity
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+
+/**
+ * Can be used to force a composable in dark theme.
+ * It will automatically change the system ui colors back to normal when leaving the composition.
+ */
+@Composable
+fun ForcedDarkElementTheme(
+ lightStatusBar: Boolean = false,
+ content: @Composable () -> Unit,
+) {
+ val colorScheme = MaterialTheme.colorScheme
+ val wasDarkTheme = !ElementTheme.colors.isLight
+ val activity = LocalActivity.current as? ComponentActivity
+ DisposableEffect(Unit) {
+ onDispose {
+ activity?.enableEdgeToEdge(
+ statusBarStyle = SystemBarStyle.auto(
+ lightScrim = colorScheme.background.toArgb(),
+ darkScrim = colorScheme.background.toArgb(),
+ ),
+ navigationBarStyle = if (wasDarkTheme) {
+ SystemBarStyle.dark(Color.Transparent.toArgb())
+ } else {
+ SystemBarStyle.light(
+ scrim = Color.Transparent.toArgb(),
+ darkScrim = Color.Transparent.toArgb()
+ )
+ }
+ )
+ }
+ }
+ ElementTheme(darkTheme = true, lightStatusBar = lightStatusBar, content = content)
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/LegacyColors.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/LegacyColors.kt
new file mode 100644
index 0000000000..6b7814d1e3
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/LegacyColors.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.compound.theme
+
+import androidx.compose.ui.graphics.Color
+import io.element.android.compound.annotations.CoreColorToken
+import io.element.android.compound.tokens.generated.internal.DarkColorTokens
+import io.element.android.compound.tokens.generated.internal.LightColorTokens
+
+// =================================================================================================
+// IMPORTANT!
+// We should not be adding any new colors here. This file is only for legacy colors.
+// In fact, we should try to remove any references to these colors as we
+// iterate through the designs. All new colors should come from Compound's Design Tokens.
+// =================================================================================================
+
+val LinkColor = Color(0xFF0086E6)
+
+@OptIn(CoreColorToken::class)
+val SnackBarLabelColorLight = LightColorTokens.colorGray700
+@OptIn(CoreColorToken::class)
+val SnackBarLabelColorDark = DarkColorTokens.colorGray700
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialColorSchemeDark.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialColorSchemeDark.kt
new file mode 100644
index 0000000000..7ee6b631b5
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialColorSchemeDark.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.compound.theme
+
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.darkColorScheme
+import io.element.android.compound.annotations.CoreColorToken
+import io.element.android.compound.tokens.generated.SemanticColors
+import io.element.android.compound.tokens.generated.internal.DarkColorTokens
+
+/**
+ * See the mapping in
+ * https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=311-14&p=f&t=QcVyNaPEZMDA6RFK-0
+ */
+@OptIn(CoreColorToken::class)
+fun SemanticColors.toMaterialColorSchemeDark(): ColorScheme = darkColorScheme(
+ primary = bgActionPrimaryRest,
+ onPrimary = textOnSolidPrimary,
+ primaryContainer = bgCanvasDefault,
+ onPrimaryContainer = textPrimary,
+ inversePrimary = textOnSolidPrimary,
+ secondary = textSecondary,
+ onSecondary = textOnSolidPrimary,
+ secondaryContainer = bgSubtlePrimary,
+ onSecondaryContainer = textPrimary,
+ tertiary = textSecondary,
+ onTertiary = textOnSolidPrimary,
+ tertiaryContainer = bgActionPrimaryRest,
+ onTertiaryContainer = textOnSolidPrimary,
+ background = bgCanvasDefault,
+ onBackground = textPrimary,
+ surface = bgCanvasDefault,
+ onSurface = textPrimary,
+ surfaceVariant = bgSubtleSecondary,
+ onSurfaceVariant = textSecondary,
+ surfaceTint = DarkColorTokens.colorGray1000,
+ inverseSurface = DarkColorTokens.colorGray1300,
+ inverseOnSurface = textOnSolidPrimary,
+ error = textCriticalPrimary,
+ onError = textOnSolidPrimary,
+ errorContainer = DarkColorTokens.colorRed400,
+ onErrorContainer = textCriticalPrimary,
+ outline = borderInteractivePrimary,
+ outlineVariant = DarkColorTokens.colorAlphaGray400,
+ // Note: for light it will be colorGray1400
+ scrim = DarkColorTokens.colorGray300,
+)
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialColorSchemeLight.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialColorSchemeLight.kt
new file mode 100644
index 0000000000..b179234429
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialColorSchemeLight.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.compound.theme
+
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.lightColorScheme
+import io.element.android.compound.annotations.CoreColorToken
+import io.element.android.compound.tokens.generated.SemanticColors
+import io.element.android.compound.tokens.generated.internal.LightColorTokens
+
+/**
+ * See the mapping in
+ * https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=311-14&p=f&t=QcVyNaPEZMDA6RFK-0
+ */
+@OptIn(CoreColorToken::class)
+fun SemanticColors.toMaterialColorSchemeLight(): ColorScheme = lightColorScheme(
+ primary = bgActionPrimaryRest,
+ onPrimary = textOnSolidPrimary,
+ primaryContainer = bgCanvasDefault,
+ onPrimaryContainer = textPrimary,
+ inversePrimary = textOnSolidPrimary,
+ secondary = textSecondary,
+ onSecondary = textOnSolidPrimary,
+ secondaryContainer = bgSubtlePrimary,
+ onSecondaryContainer = textPrimary,
+ tertiary = textSecondary,
+ onTertiary = textOnSolidPrimary,
+ tertiaryContainer = bgActionPrimaryRest,
+ onTertiaryContainer = textOnSolidPrimary,
+ background = bgCanvasDefault,
+ onBackground = textPrimary,
+ surface = bgCanvasDefault,
+ onSurface = textPrimary,
+ surfaceVariant = bgSubtleSecondary,
+ onSurfaceVariant = textSecondary,
+ surfaceTint = LightColorTokens.colorGray1000,
+ inverseSurface = LightColorTokens.colorGray1300,
+ inverseOnSurface = textOnSolidPrimary,
+ error = textCriticalPrimary,
+ onError = textOnSolidPrimary,
+ errorContainer = LightColorTokens.colorRed400,
+ onErrorContainer = textCriticalPrimary,
+ outline = borderInteractivePrimary,
+ outlineVariant = LightColorTokens.colorAlphaGray400,
+ // Note: for dark it will be colorGray300
+ scrim = LightColorTokens.colorGray1400,
+)
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialTextPreview.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialTextPreview.kt
new file mode 100644
index 0000000000..724d440161
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialTextPreview.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.theme
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import io.element.android.compound.utils.toHrf
+
+@Preview(heightDp = 1200, widthDp = 420)
+@Composable
+internal fun MaterialTextPreview() = Row(
+ modifier = Modifier.background(Color.Yellow)
+) {
+ MaterialPreview(
+ modifier = Modifier.weight(1f),
+ darkTheme = false,
+ )
+ MaterialPreview(
+ modifier = Modifier.weight(1f),
+ darkTheme = true,
+ )
+}
+
+private data class Model(
+ val name: String,
+ val bgColor: Color,
+ val textColor: Color,
+)
+
+@Composable
+private fun MaterialPreview(
+ darkTheme: Boolean,
+ modifier: Modifier = Modifier,
+) = Column(modifier = modifier) {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ textAlign = TextAlign.Center,
+ text = if (darkTheme) "Dark" else "Light",
+ color = Color.Black,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ )
+ ElementTheme(
+ darkTheme = darkTheme,
+ ) {
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ listOf(
+ Model("Background", MaterialTheme.colorScheme.background, MaterialTheme.colorScheme.onBackground),
+ Model("Primary", MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.onPrimary),
+ Model("PrimaryContainer", MaterialTheme.colorScheme.primaryContainer, MaterialTheme.colorScheme.onPrimaryContainer),
+ Model("Secondary", MaterialTheme.colorScheme.secondary, MaterialTheme.colorScheme.onSecondary),
+ Model("SecondaryContainer", MaterialTheme.colorScheme.secondaryContainer, MaterialTheme.colorScheme.onSecondaryContainer),
+ Model("Tertiary", MaterialTheme.colorScheme.tertiary, MaterialTheme.colorScheme.onTertiary),
+ Model("TertiaryContainer", MaterialTheme.colorScheme.tertiaryContainer, MaterialTheme.colorScheme.onTertiaryContainer),
+ Model("Surface", MaterialTheme.colorScheme.surface, MaterialTheme.colorScheme.onSurface),
+ Model("SurfaceVariant", MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant),
+ Model("InverseSurface", MaterialTheme.colorScheme.inverseSurface, MaterialTheme.colorScheme.inverseOnSurface),
+ Model("Error", MaterialTheme.colorScheme.error, MaterialTheme.colorScheme.onError),
+ Model("ErrorContainer", MaterialTheme.colorScheme.errorContainer, MaterialTheme.colorScheme.onErrorContainer),
+ ).forEach {
+ TextPreview(
+ name = it.name,
+ bgColor = it.bgColor,
+ textColor = it.textColor,
+ )
+ }
+ Box(
+ modifier = Modifier
+ .padding(1.dp)
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.background)
+ ) {
+ Text(
+ text = "Below\n".repeat(3),
+ color = MaterialTheme.colorScheme.onBackground,
+ )
+ Text(
+ modifier = Modifier
+ .padding(12.dp)
+ .fillMaxWidth()
+ // the alpha applied to the scrim color does not seem to be mandatory.
+ // The library ignores the alpha level provided and apply it's own.
+ // For testing the color, manually set an alpha.
+ .background(color = MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f))
+ .padding(16.dp),
+ text = "${"Scrim"}\n${MaterialTheme.colorScheme.scrim.toHrf()}",
+ style = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace),
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.onBackground,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun TextPreview(
+ name: String,
+ bgColor: Color,
+ textColor: Color,
+ modifier: Modifier = Modifier,
+) = Text(
+ modifier = modifier
+ .padding(1.dp)
+ .fillMaxWidth()
+ .background(bgColor)
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ text = "$name\n${textColor.toHrf()}\n${bgColor.toHrf()}",
+ style = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace),
+ textAlign = TextAlign.Center,
+ color = textColor,
+)
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialThemeColors.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialThemeColors.kt
new file mode 100644
index 0000000000..f81b0a94f6
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialThemeColors.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.compound.theme
+
+import androidx.compose.material3.ColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import io.element.android.compound.previews.ColorsSchemePreview
+import io.element.android.compound.tokens.generated.SemanticColors
+import io.element.android.compound.tokens.generated.compoundColorsHcDark
+import io.element.android.compound.tokens.generated.compoundColorsHcLight
+
+fun SemanticColors.toMaterialColorScheme(): ColorScheme {
+ return if (isLight) {
+ toMaterialColorSchemeLight()
+ } else {
+ toMaterialColorSchemeDark()
+ }
+}
+
+@Preview(heightDp = 1200)
+@Composable
+internal fun ColorsSchemeLightPreview() = ElementTheme {
+ ColorsSchemePreview(
+ Color.Black,
+ Color.White,
+ ElementTheme.materialColors,
+ )
+}
+
+@Preview(heightDp = 1200)
+@Composable
+internal fun ColorsSchemeLightHcPreview() = ElementTheme(
+ compoundLight = compoundColorsHcLight,
+) {
+ ColorsSchemePreview(
+ Color.Black,
+ Color.White,
+ ElementTheme.materialColors,
+ )
+}
+
+@Preview(heightDp = 1200)
+@Composable
+internal fun ColorsSchemeDarkPreview() = ElementTheme(
+ darkTheme = true,
+) {
+ ColorsSchemePreview(
+ Color.White,
+ Color.Black,
+ ElementTheme.materialColors,
+ )
+}
+
+@Preview(heightDp = 1200)
+@Composable
+internal fun ColorsSchemeDarkHcPreview() = ElementTheme(
+ darkTheme = true,
+ compoundDark = compoundColorsHcDark,
+) {
+ ColorsSchemePreview(
+ Color.White,
+ Color.Black,
+ ElementTheme.materialColors,
+ )
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt
new file mode 100644
index 0000000000..06668e2952
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.runtime.Composable
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+enum class Theme {
+ System,
+ Dark,
+ Light,
+}
+
+@Composable
+fun Theme.isDark(): Boolean {
+ return when (this) {
+ Theme.System -> isSystemInDarkTheme()
+ Theme.Dark -> true
+ Theme.Light -> false
+ }
+}
+
+fun Flow.mapToTheme(): Flow = map {
+ when (it) {
+ null -> Theme.System
+ else -> Theme.valueOf(it)
+ }
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/CompoundTypography.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/CompoundTypography.kt
new file mode 100644
index 0000000000..ab4d898ba4
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/CompoundTypography.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.compound.tokens
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.PlatformTextStyle
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
+import com.airbnb.android.showkase.annotation.ShowkaseTypography
+import io.element.android.compound.tokens.generated.TypographyTokens
+
+// 32px (Material) vs 34px, it's the closest one
+@ShowkaseTypography(name = "M3 Headline Large", group = "Compound")
+internal val compoundHeadingXlRegular = TypographyTokens.fontHeadingXlRegular
+
+// both are 28px
+@ShowkaseTypography(name = "M3 Headline Medium", group = "Compound")
+internal val compoundHeadingLgRegular = TypographyTokens.fontHeadingLgRegular
+
+// These are the default M3 values, but we're setting them manually so an update in M3 doesn't break our designs
+@ShowkaseTypography(name = "M3 Headline Small", group = "Compound")
+internal val defaultHeadlineSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ lineHeight = 32.sp,
+ fontSize = 24.sp,
+ letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+)
+
+// 22px (Material) vs 20px, it's the closest one
+@ShowkaseTypography(name = "M3 Title Large", group = "Compound")
+internal val compoundHeadingMdRegular = TypographyTokens.fontHeadingMdRegular
+
+// 16px both
+@ShowkaseTypography(name = "M3 Title Medium", group = "Compound")
+internal val compoundBodyLgMedium = TypographyTokens.fontBodyLgMedium
+
+// 14px both
+@ShowkaseTypography(name = "M3 Title Small", group = "Compound")
+internal val compoundBodyMdMedium = TypographyTokens.fontBodyMdMedium
+
+// 16px both
+@ShowkaseTypography(name = "M3 Body Large", group = "Compound")
+internal val compoundBodyLgRegular = TypographyTokens.fontBodyLgRegular
+
+// 14px both
+@ShowkaseTypography(name = "M3 Body Medium", group = "Compound")
+internal val compoundBodyMdRegular = TypographyTokens.fontBodyMdRegular
+
+// 12px both
+@ShowkaseTypography(name = "M3 Body Small", group = "Compound")
+internal val compoundBodySmRegular = TypographyTokens.fontBodySmRegular
+
+// 14px both, Title Small uses the same token so we have to declare it twice
+@ShowkaseTypography(name = "M3 Label Large", group = "Compound")
+internal val compoundBodyMdMedium_LabelLarge = TypographyTokens.fontBodyMdMedium
+
+// 12px both
+@ShowkaseTypography(name = "M3 Label Medium", group = "Compound")
+internal val compoundBodySmMedium = TypographyTokens.fontBodySmMedium
+
+// 11px both
+@ShowkaseTypography(name = "M3 Label Small", group = "Compound")
+internal val compoundBodyXsMedium = TypographyTokens.fontBodyXsMedium
+
+internal val compoundTypography = Typography(
+ // displayLarge = , 57px (Material) size. We have no equivalent
+ // displayMedium = , 45px (Material) size. We have no equivalent
+ // displaySmall = , 36px (Material) size. We have no equivalent
+ headlineLarge = compoundHeadingXlRegular,
+ headlineMedium = compoundHeadingLgRegular,
+ headlineSmall = defaultHeadlineSmall,
+ titleLarge = compoundHeadingMdRegular,
+ titleMedium = compoundBodyLgMedium,
+ titleSmall = compoundBodyMdMedium,
+ bodyLarge = compoundBodyLgRegular,
+ bodyMedium = compoundBodyMdRegular,
+ bodySmall = compoundBodySmRegular,
+ labelLarge = compoundBodyMdMedium_LabelLarge,
+ labelMedium = compoundBodySmMedium,
+ labelSmall = compoundBodyXsMedium,
+)
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/CompoundIcons.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/CompoundIcons.kt
new file mode 100644
index 0000000000..76b3275651
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/CompoundIcons.kt
@@ -0,0 +1,1034 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+
+/**
+ * !!! WARNING !!!
+ *
+ * THIS IS AN AUTOGENERATED FILE.
+ * DO NOT EDIT MANUALLY.
+ */
+
+
+
+@file:Suppress("all")
+package io.element.android.compound.tokens.generated
+
+import io.element.android.compound.R
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
+import kotlinx.collections.immutable.persistentListOf
+
+object CompoundIcons {
+ @Composable fun Admin(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_admin)
+ }
+ @Composable fun ArrowDown(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_arrow_down)
+ }
+ @Composable fun ArrowLeft(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_arrow_left)
+ }
+ @Composable fun ArrowRight(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_arrow_right)
+ }
+ @Composable fun ArrowUp(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_arrow_up)
+ }
+ @Composable fun ArrowUpRight(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_arrow_up_right)
+ }
+ @Composable fun AskToJoin(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_ask_to_join)
+ }
+ @Composable fun AskToJoinSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_ask_to_join_solid)
+ }
+ @Composable fun Attachment(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_attachment)
+ }
+ @Composable fun Audio(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_audio)
+ }
+ @Composable fun Block(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_block)
+ }
+ @Composable fun Bold(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_bold)
+ }
+ @Composable fun Calendar(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_calendar)
+ }
+ @Composable fun Chart(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_chart)
+ }
+ @Composable fun Chat(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_chat)
+ }
+ @Composable fun ChatNew(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_chat_new)
+ }
+ @Composable fun ChatProblem(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_chat_problem)
+ }
+ @Composable fun ChatSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_chat_solid)
+ }
+ @Composable fun Check(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_check)
+ }
+ @Composable fun CheckCircle(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_check_circle)
+ }
+ @Composable fun CheckCircleSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_check_circle_solid)
+ }
+ @Composable fun ChevronDown(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_chevron_down)
+ }
+ @Composable fun ChevronLeft(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_chevron_left)
+ }
+ @Composable fun ChevronRight(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_chevron_right)
+ }
+ @Composable fun ChevronUp(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_chevron_up)
+ }
+ @Composable fun ChevronUpDown(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_chevron_up_down)
+ }
+ @Composable fun Circle(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_circle)
+ }
+ @Composable fun Close(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_close)
+ }
+ @Composable fun Cloud(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_cloud)
+ }
+ @Composable fun CloudSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_cloud_solid)
+ }
+ @Composable fun Code(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_code)
+ }
+ @Composable fun Collapse(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_collapse)
+ }
+ @Composable fun Company(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_company)
+ }
+ @Composable fun Compose(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_compose)
+ }
+ @Composable fun Computer(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_computer)
+ }
+ @Composable fun Copy(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_copy)
+ }
+ @Composable fun DarkMode(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_dark_mode)
+ }
+ @Composable fun Delete(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_delete)
+ }
+ @Composable fun Devices(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_devices)
+ }
+ @Composable fun DialPad(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_dial_pad)
+ }
+ @Composable fun Document(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_document)
+ }
+ @Composable fun Download(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_download)
+ }
+ @Composable fun DownloadIos(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_download_ios)
+ }
+ @Composable fun DragGrid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_drag_grid)
+ }
+ @Composable fun DragList(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_drag_list)
+ }
+ @Composable fun Earpiece(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_earpiece)
+ }
+ @Composable fun Edit(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_edit)
+ }
+ @Composable fun EditSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_edit_solid)
+ }
+ @Composable fun Email(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_email)
+ }
+ @Composable fun EmailSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_email_solid)
+ }
+ @Composable fun EndCall(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_end_call)
+ }
+ @Composable fun Error(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_error)
+ }
+ @Composable fun ErrorSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_error_solid)
+ }
+ @Composable fun Expand(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_expand)
+ }
+ @Composable fun Explore(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_explore)
+ }
+ @Composable fun ExportArchive(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_export_archive)
+ }
+ @Composable fun Extensions(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_extensions)
+ }
+ @Composable fun ExtensionsSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_extensions_solid)
+ }
+ @Composable fun Favourite(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_favourite)
+ }
+ @Composable fun FavouriteSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_favourite_solid)
+ }
+ @Composable fun FileError(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_file_error)
+ }
+ @Composable fun Files(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_files)
+ }
+ @Composable fun Filter(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_filter)
+ }
+ @Composable fun Forward(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_forward)
+ }
+ @Composable fun Grid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_grid)
+ }
+ @Composable fun Group(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_group)
+ }
+ @Composable fun Guest(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_guest)
+ }
+ @Composable fun HeadphonesOffSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_headphones_off_solid)
+ }
+ @Composable fun HeadphonesSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_headphones_solid)
+ }
+ @Composable fun Help(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_help)
+ }
+ @Composable fun HelpSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_help_solid)
+ }
+ @Composable fun History(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_history)
+ }
+ @Composable fun Home(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_home)
+ }
+ @Composable fun HomeSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_home_solid)
+ }
+ @Composable fun Host(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_host)
+ }
+ @Composable fun Image(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_image)
+ }
+ @Composable fun ImageError(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_image_error)
+ }
+ @Composable fun IndentDecrease(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_indent_decrease)
+ }
+ @Composable fun IndentIncrease(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_indent_increase)
+ }
+ @Composable fun Info(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_info)
+ }
+ @Composable fun InfoSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_info_solid)
+ }
+ @Composable fun InlineCode(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_inline_code)
+ }
+ @Composable fun Italic(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_italic)
+ }
+ @Composable fun Key(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_key)
+ }
+ @Composable fun KeyOff(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_key_off)
+ }
+ @Composable fun KeyOffSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_key_off_solid)
+ }
+ @Composable fun KeySolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_key_solid)
+ }
+ @Composable fun Keyboard(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_keyboard)
+ }
+ @Composable fun Labs(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_labs)
+ }
+ @Composable fun Leave(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_leave)
+ }
+ @Composable fun Link(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_link)
+ }
+ @Composable fun Linux(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_linux)
+ }
+ @Composable fun ListBulleted(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_list_bulleted)
+ }
+ @Composable fun ListNumbered(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_list_numbered)
+ }
+ @Composable fun ListView(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_list_view)
+ }
+ @Composable fun LocationNavigator(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_location_navigator)
+ }
+ @Composable fun LocationNavigatorCentred(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_location_navigator_centred)
+ }
+ @Composable fun LocationPin(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_location_pin)
+ }
+ @Composable fun LocationPinSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_location_pin_solid)
+ }
+ @Composable fun Lock(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_lock)
+ }
+ @Composable fun LockOff(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_lock_off)
+ }
+ @Composable fun LockSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_lock_solid)
+ }
+ @Composable fun Mac(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_mac)
+ }
+ @Composable fun MarkAsRead(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_mark_as_read)
+ }
+ @Composable fun MarkAsUnread(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_mark_as_unread)
+ }
+ @Composable fun MarkThreadsAsRead(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_mark_threads_as_read)
+ }
+ @Composable fun MarkerReadReceipts(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_marker_read_receipts)
+ }
+ @Composable fun Mention(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_mention)
+ }
+ @Composable fun Menu(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_menu)
+ }
+ @Composable fun MicOff(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_mic_off)
+ }
+ @Composable fun MicOffSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_mic_off_solid)
+ }
+ @Composable fun MicOn(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_mic_on)
+ }
+ @Composable fun MicOnSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_mic_on_solid)
+ }
+ @Composable fun Minus(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_minus)
+ }
+ @Composable fun Mobile(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_mobile)
+ }
+ @Composable fun Notifications(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_notifications)
+ }
+ @Composable fun NotificationsOff(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_notifications_off)
+ }
+ @Composable fun NotificationsOffSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_notifications_off_solid)
+ }
+ @Composable fun NotificationsSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_notifications_solid)
+ }
+ @Composable fun Offline(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_offline)
+ }
+ @Composable fun OverflowHorizontal(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_overflow_horizontal)
+ }
+ @Composable fun OverflowVertical(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_overflow_vertical)
+ }
+ @Composable fun Pause(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_pause)
+ }
+ @Composable fun PauseSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_pause_solid)
+ }
+ @Composable fun Pin(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_pin)
+ }
+ @Composable fun PinSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_pin_solid)
+ }
+ @Composable fun Play(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_play)
+ }
+ @Composable fun PlaySolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_play_solid)
+ }
+ @Composable fun Plus(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_plus)
+ }
+ @Composable fun Polls(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_polls)
+ }
+ @Composable fun PollsEnd(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_polls_end)
+ }
+ @Composable fun PopOut(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_pop_out)
+ }
+ @Composable fun Preferences(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_preferences)
+ }
+ @Composable fun PresenceOutline8X8(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_presence_outline_8_x_8)
+ }
+ @Composable fun PresenceSolid8X8(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_presence_solid_8_x_8)
+ }
+ @Composable fun PresenceStrikethrough8X8(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_presence_strikethrough_8_x_8)
+ }
+ @Composable fun Public(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_public)
+ }
+ @Composable fun QrCode(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_qr_code)
+ }
+ @Composable fun Quote(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_quote)
+ }
+ @Composable fun RaisedHandSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_raised_hand_solid)
+ }
+ @Composable fun Reaction(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_reaction)
+ }
+ @Composable fun ReactionAdd(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_reaction_add)
+ }
+ @Composable fun ReactionSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_reaction_solid)
+ }
+ @Composable fun Reply(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_reply)
+ }
+ @Composable fun Restart(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_restart)
+ }
+ @Composable fun Room(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_room)
+ }
+ @Composable fun Search(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_search)
+ }
+ @Composable fun Send(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_send)
+ }
+ @Composable fun SendSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_send_solid)
+ }
+ @Composable fun Settings(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_settings)
+ }
+ @Composable fun SettingsSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_settings_solid)
+ }
+ @Composable fun Share(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_share)
+ }
+ @Composable fun ShareAndroid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_share_android)
+ }
+ @Composable fun ShareIos(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_share_ios)
+ }
+ @Composable fun ShareScreen(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_share_screen)
+ }
+ @Composable fun ShareScreenSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_share_screen_solid)
+ }
+ @Composable fun Shield(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_shield)
+ }
+ @Composable fun Sidebar(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_sidebar)
+ }
+ @Composable fun SignOut(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_sign_out)
+ }
+ @Composable fun Spinner(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_spinner)
+ }
+ @Composable fun Spotlight(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_spotlight)
+ }
+ @Composable fun SpotlightView(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_spotlight_view)
+ }
+ @Composable fun Strikethrough(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_strikethrough)
+ }
+ @Composable fun SwitchCameraSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_switch_camera_solid)
+ }
+ @Composable fun TakePhoto(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_take_photo)
+ }
+ @Composable fun TakePhotoSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_take_photo_solid)
+ }
+ @Composable fun TextFormatting(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_text_formatting)
+ }
+ @Composable fun Threads(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_threads)
+ }
+ @Composable fun ThreadsSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_threads_solid)
+ }
+ @Composable fun Time(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_time)
+ }
+ @Composable fun Underline(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_underline)
+ }
+ @Composable fun Unknown(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_unknown)
+ }
+ @Composable fun UnknownSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_unknown_solid)
+ }
+ @Composable fun Unpin(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_unpin)
+ }
+ @Composable fun User(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_user)
+ }
+ @Composable fun UserAdd(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_user_add)
+ }
+ @Composable fun UserAddSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_user_add_solid)
+ }
+ @Composable fun UserProfile(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_user_profile)
+ }
+ @Composable fun UserProfileSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_user_profile_solid)
+ }
+ @Composable fun UserSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_user_solid)
+ }
+ @Composable fun Verified(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_verified)
+ }
+ @Composable fun VideoCall(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_video_call)
+ }
+ @Composable fun VideoCallDeclinedSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_video_call_declined_solid)
+ }
+ @Composable fun VideoCallMissedSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_video_call_missed_solid)
+ }
+ @Composable fun VideoCallOff(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_video_call_off)
+ }
+ @Composable fun VideoCallOffSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_video_call_off_solid)
+ }
+ @Composable fun VideoCallSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_video_call_solid)
+ }
+ @Composable fun VisibilityOff(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_visibility_off)
+ }
+ @Composable fun VisibilityOn(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_visibility_on)
+ }
+ @Composable fun VoiceCall(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_voice_call)
+ }
+ @Composable fun VoiceCallSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_voice_call_solid)
+ }
+ @Composable fun VolumeOff(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_volume_off)
+ }
+ @Composable fun VolumeOffSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_volume_off_solid)
+ }
+ @Composable fun VolumeOn(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_volume_on)
+ }
+ @Composable fun VolumeOnSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_volume_on_solid)
+ }
+ @Composable fun Warning(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_warning)
+ }
+ @Composable fun WebBrowser(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_web_browser)
+ }
+ @Composable fun Windows(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_windows)
+ }
+ @Composable fun Workspace(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_workspace)
+ }
+ @Composable fun WorkspaceSolid(): ImageVector {
+ return ImageVector.vectorResource(R.drawable.ic_compound_workspace_solid)
+ }
+
+ val all @Composable get() = persistentListOf(
+ Admin(),
+ ArrowDown(),
+ ArrowLeft(),
+ ArrowRight(),
+ ArrowUp(),
+ ArrowUpRight(),
+ AskToJoin(),
+ AskToJoinSolid(),
+ Attachment(),
+ Audio(),
+ Block(),
+ Bold(),
+ Calendar(),
+ Chart(),
+ Chat(),
+ ChatNew(),
+ ChatProblem(),
+ ChatSolid(),
+ Check(),
+ CheckCircle(),
+ CheckCircleSolid(),
+ ChevronDown(),
+ ChevronLeft(),
+ ChevronRight(),
+ ChevronUp(),
+ ChevronUpDown(),
+ Circle(),
+ Close(),
+ Cloud(),
+ CloudSolid(),
+ Code(),
+ Collapse(),
+ Company(),
+ Compose(),
+ Computer(),
+ Copy(),
+ DarkMode(),
+ Delete(),
+ Devices(),
+ DialPad(),
+ Document(),
+ Download(),
+ DownloadIos(),
+ DragGrid(),
+ DragList(),
+ Earpiece(),
+ Edit(),
+ EditSolid(),
+ Email(),
+ EmailSolid(),
+ EndCall(),
+ Error(),
+ ErrorSolid(),
+ Expand(),
+ Explore(),
+ ExportArchive(),
+ Extensions(),
+ ExtensionsSolid(),
+ Favourite(),
+ FavouriteSolid(),
+ FileError(),
+ Files(),
+ Filter(),
+ Forward(),
+ Grid(),
+ Group(),
+ Guest(),
+ HeadphonesOffSolid(),
+ HeadphonesSolid(),
+ Help(),
+ HelpSolid(),
+ History(),
+ Home(),
+ HomeSolid(),
+ Host(),
+ Image(),
+ ImageError(),
+ IndentDecrease(),
+ IndentIncrease(),
+ Info(),
+ InfoSolid(),
+ InlineCode(),
+ Italic(),
+ Key(),
+ KeyOff(),
+ KeyOffSolid(),
+ KeySolid(),
+ Keyboard(),
+ Labs(),
+ Leave(),
+ Link(),
+ Linux(),
+ ListBulleted(),
+ ListNumbered(),
+ ListView(),
+ LocationNavigator(),
+ LocationNavigatorCentred(),
+ LocationPin(),
+ LocationPinSolid(),
+ Lock(),
+ LockOff(),
+ LockSolid(),
+ Mac(),
+ MarkAsRead(),
+ MarkAsUnread(),
+ MarkThreadsAsRead(),
+ MarkerReadReceipts(),
+ Mention(),
+ Menu(),
+ MicOff(),
+ MicOffSolid(),
+ MicOn(),
+ MicOnSolid(),
+ Minus(),
+ Mobile(),
+ Notifications(),
+ NotificationsOff(),
+ NotificationsOffSolid(),
+ NotificationsSolid(),
+ Offline(),
+ OverflowHorizontal(),
+ OverflowVertical(),
+ Pause(),
+ PauseSolid(),
+ Pin(),
+ PinSolid(),
+ Play(),
+ PlaySolid(),
+ Plus(),
+ Polls(),
+ PollsEnd(),
+ PopOut(),
+ Preferences(),
+ PresenceOutline8X8(),
+ PresenceSolid8X8(),
+ PresenceStrikethrough8X8(),
+ Public(),
+ QrCode(),
+ Quote(),
+ RaisedHandSolid(),
+ Reaction(),
+ ReactionAdd(),
+ ReactionSolid(),
+ Reply(),
+ Restart(),
+ Room(),
+ Search(),
+ Send(),
+ SendSolid(),
+ Settings(),
+ SettingsSolid(),
+ Share(),
+ ShareAndroid(),
+ ShareIos(),
+ ShareScreen(),
+ ShareScreenSolid(),
+ Shield(),
+ Sidebar(),
+ SignOut(),
+ Spinner(),
+ Spotlight(),
+ SpotlightView(),
+ Strikethrough(),
+ SwitchCameraSolid(),
+ TakePhoto(),
+ TakePhotoSolid(),
+ TextFormatting(),
+ Threads(),
+ ThreadsSolid(),
+ Time(),
+ Underline(),
+ Unknown(),
+ UnknownSolid(),
+ Unpin(),
+ User(),
+ UserAdd(),
+ UserAddSolid(),
+ UserProfile(),
+ UserProfileSolid(),
+ UserSolid(),
+ Verified(),
+ VideoCall(),
+ VideoCallDeclinedSolid(),
+ VideoCallMissedSolid(),
+ VideoCallOff(),
+ VideoCallOffSolid(),
+ VideoCallSolid(),
+ VisibilityOff(),
+ VisibilityOn(),
+ VoiceCall(),
+ VoiceCallSolid(),
+ VolumeOff(),
+ VolumeOffSolid(),
+ VolumeOn(),
+ VolumeOnSolid(),
+ Warning(),
+ WebBrowser(),
+ Windows(),
+ Workspace(),
+ WorkspaceSolid(),
+ )
+
+ val allResIds get() = persistentListOf(
+ R.drawable.ic_compound_admin,
+ R.drawable.ic_compound_arrow_down,
+ R.drawable.ic_compound_arrow_left,
+ R.drawable.ic_compound_arrow_right,
+ R.drawable.ic_compound_arrow_up,
+ R.drawable.ic_compound_arrow_up_right,
+ R.drawable.ic_compound_ask_to_join,
+ R.drawable.ic_compound_ask_to_join_solid,
+ R.drawable.ic_compound_attachment,
+ R.drawable.ic_compound_audio,
+ R.drawable.ic_compound_block,
+ R.drawable.ic_compound_bold,
+ R.drawable.ic_compound_calendar,
+ R.drawable.ic_compound_chart,
+ R.drawable.ic_compound_chat,
+ R.drawable.ic_compound_chat_new,
+ R.drawable.ic_compound_chat_problem,
+ R.drawable.ic_compound_chat_solid,
+ R.drawable.ic_compound_check,
+ R.drawable.ic_compound_check_circle,
+ R.drawable.ic_compound_check_circle_solid,
+ R.drawable.ic_compound_chevron_down,
+ R.drawable.ic_compound_chevron_left,
+ R.drawable.ic_compound_chevron_right,
+ R.drawable.ic_compound_chevron_up,
+ R.drawable.ic_compound_chevron_up_down,
+ R.drawable.ic_compound_circle,
+ R.drawable.ic_compound_close,
+ R.drawable.ic_compound_cloud,
+ R.drawable.ic_compound_cloud_solid,
+ R.drawable.ic_compound_code,
+ R.drawable.ic_compound_collapse,
+ R.drawable.ic_compound_company,
+ R.drawable.ic_compound_compose,
+ R.drawable.ic_compound_computer,
+ R.drawable.ic_compound_copy,
+ R.drawable.ic_compound_dark_mode,
+ R.drawable.ic_compound_delete,
+ R.drawable.ic_compound_devices,
+ R.drawable.ic_compound_dial_pad,
+ R.drawable.ic_compound_document,
+ R.drawable.ic_compound_download,
+ R.drawable.ic_compound_download_ios,
+ R.drawable.ic_compound_drag_grid,
+ R.drawable.ic_compound_drag_list,
+ R.drawable.ic_compound_earpiece,
+ R.drawable.ic_compound_edit,
+ R.drawable.ic_compound_edit_solid,
+ R.drawable.ic_compound_email,
+ R.drawable.ic_compound_email_solid,
+ R.drawable.ic_compound_end_call,
+ R.drawable.ic_compound_error,
+ R.drawable.ic_compound_error_solid,
+ R.drawable.ic_compound_expand,
+ R.drawable.ic_compound_explore,
+ R.drawable.ic_compound_export_archive,
+ R.drawable.ic_compound_extensions,
+ R.drawable.ic_compound_extensions_solid,
+ R.drawable.ic_compound_favourite,
+ R.drawable.ic_compound_favourite_solid,
+ R.drawable.ic_compound_file_error,
+ R.drawable.ic_compound_files,
+ R.drawable.ic_compound_filter,
+ R.drawable.ic_compound_forward,
+ R.drawable.ic_compound_grid,
+ R.drawable.ic_compound_group,
+ R.drawable.ic_compound_guest,
+ R.drawable.ic_compound_headphones_off_solid,
+ R.drawable.ic_compound_headphones_solid,
+ R.drawable.ic_compound_help,
+ R.drawable.ic_compound_help_solid,
+ R.drawable.ic_compound_history,
+ R.drawable.ic_compound_home,
+ R.drawable.ic_compound_home_solid,
+ R.drawable.ic_compound_host,
+ R.drawable.ic_compound_image,
+ R.drawable.ic_compound_image_error,
+ R.drawable.ic_compound_indent_decrease,
+ R.drawable.ic_compound_indent_increase,
+ R.drawable.ic_compound_info,
+ R.drawable.ic_compound_info_solid,
+ R.drawable.ic_compound_inline_code,
+ R.drawable.ic_compound_italic,
+ R.drawable.ic_compound_key,
+ R.drawable.ic_compound_key_off,
+ R.drawable.ic_compound_key_off_solid,
+ R.drawable.ic_compound_key_solid,
+ R.drawable.ic_compound_keyboard,
+ R.drawable.ic_compound_labs,
+ R.drawable.ic_compound_leave,
+ R.drawable.ic_compound_link,
+ R.drawable.ic_compound_linux,
+ R.drawable.ic_compound_list_bulleted,
+ R.drawable.ic_compound_list_numbered,
+ R.drawable.ic_compound_list_view,
+ R.drawable.ic_compound_location_navigator,
+ R.drawable.ic_compound_location_navigator_centred,
+ R.drawable.ic_compound_location_pin,
+ R.drawable.ic_compound_location_pin_solid,
+ R.drawable.ic_compound_lock,
+ R.drawable.ic_compound_lock_off,
+ R.drawable.ic_compound_lock_solid,
+ R.drawable.ic_compound_mac,
+ R.drawable.ic_compound_mark_as_read,
+ R.drawable.ic_compound_mark_as_unread,
+ R.drawable.ic_compound_mark_threads_as_read,
+ R.drawable.ic_compound_marker_read_receipts,
+ R.drawable.ic_compound_mention,
+ R.drawable.ic_compound_menu,
+ R.drawable.ic_compound_mic_off,
+ R.drawable.ic_compound_mic_off_solid,
+ R.drawable.ic_compound_mic_on,
+ R.drawable.ic_compound_mic_on_solid,
+ R.drawable.ic_compound_minus,
+ R.drawable.ic_compound_mobile,
+ R.drawable.ic_compound_notifications,
+ R.drawable.ic_compound_notifications_off,
+ R.drawable.ic_compound_notifications_off_solid,
+ R.drawable.ic_compound_notifications_solid,
+ R.drawable.ic_compound_offline,
+ R.drawable.ic_compound_overflow_horizontal,
+ R.drawable.ic_compound_overflow_vertical,
+ R.drawable.ic_compound_pause,
+ R.drawable.ic_compound_pause_solid,
+ R.drawable.ic_compound_pin,
+ R.drawable.ic_compound_pin_solid,
+ R.drawable.ic_compound_play,
+ R.drawable.ic_compound_play_solid,
+ R.drawable.ic_compound_plus,
+ R.drawable.ic_compound_polls,
+ R.drawable.ic_compound_polls_end,
+ R.drawable.ic_compound_pop_out,
+ R.drawable.ic_compound_preferences,
+ R.drawable.ic_compound_presence_outline_8_x_8,
+ R.drawable.ic_compound_presence_solid_8_x_8,
+ R.drawable.ic_compound_presence_strikethrough_8_x_8,
+ R.drawable.ic_compound_public,
+ R.drawable.ic_compound_qr_code,
+ R.drawable.ic_compound_quote,
+ R.drawable.ic_compound_raised_hand_solid,
+ R.drawable.ic_compound_reaction,
+ R.drawable.ic_compound_reaction_add,
+ R.drawable.ic_compound_reaction_solid,
+ R.drawable.ic_compound_reply,
+ R.drawable.ic_compound_restart,
+ R.drawable.ic_compound_room,
+ R.drawable.ic_compound_search,
+ R.drawable.ic_compound_send,
+ R.drawable.ic_compound_send_solid,
+ R.drawable.ic_compound_settings,
+ R.drawable.ic_compound_settings_solid,
+ R.drawable.ic_compound_share,
+ R.drawable.ic_compound_share_android,
+ R.drawable.ic_compound_share_ios,
+ R.drawable.ic_compound_share_screen,
+ R.drawable.ic_compound_share_screen_solid,
+ R.drawable.ic_compound_shield,
+ R.drawable.ic_compound_sidebar,
+ R.drawable.ic_compound_sign_out,
+ R.drawable.ic_compound_spinner,
+ R.drawable.ic_compound_spotlight,
+ R.drawable.ic_compound_spotlight_view,
+ R.drawable.ic_compound_strikethrough,
+ R.drawable.ic_compound_switch_camera_solid,
+ R.drawable.ic_compound_take_photo,
+ R.drawable.ic_compound_take_photo_solid,
+ R.drawable.ic_compound_text_formatting,
+ R.drawable.ic_compound_threads,
+ R.drawable.ic_compound_threads_solid,
+ R.drawable.ic_compound_time,
+ R.drawable.ic_compound_underline,
+ R.drawable.ic_compound_unknown,
+ R.drawable.ic_compound_unknown_solid,
+ R.drawable.ic_compound_unpin,
+ R.drawable.ic_compound_user,
+ R.drawable.ic_compound_user_add,
+ R.drawable.ic_compound_user_add_solid,
+ R.drawable.ic_compound_user_profile,
+ R.drawable.ic_compound_user_profile_solid,
+ R.drawable.ic_compound_user_solid,
+ R.drawable.ic_compound_verified,
+ R.drawable.ic_compound_video_call,
+ R.drawable.ic_compound_video_call_declined_solid,
+ R.drawable.ic_compound_video_call_missed_solid,
+ R.drawable.ic_compound_video_call_off,
+ R.drawable.ic_compound_video_call_off_solid,
+ R.drawable.ic_compound_video_call_solid,
+ R.drawable.ic_compound_visibility_off,
+ R.drawable.ic_compound_visibility_on,
+ R.drawable.ic_compound_voice_call,
+ R.drawable.ic_compound_voice_call_solid,
+ R.drawable.ic_compound_volume_off,
+ R.drawable.ic_compound_volume_off_solid,
+ R.drawable.ic_compound_volume_on,
+ R.drawable.ic_compound_volume_on_solid,
+ R.drawable.ic_compound_warning,
+ R.drawable.ic_compound_web_browser,
+ R.drawable.ic_compound_windows,
+ R.drawable.ic_compound_workspace,
+ R.drawable.ic_compound_workspace_solid,
+ )
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/DO_NOT_MODIFY.txt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/DO_NOT_MODIFY.txt
new file mode 100644
index 0000000000..a6f7dd3f6a
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/DO_NOT_MODIFY.txt
@@ -0,0 +1 @@
+Files inside this package are generated automatically from the Compound project (https://github.com/vector-im/compound-design-tokens) and will be batch-replaced when new tokens are generated.
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColors.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColors.kt
new file mode 100644
index 0000000000..8da51213f8
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColors.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.
+ */
+
+
+@file:Suppress("all")
+package io.element.android.compound.tokens.generated
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
+
+
+/**
+ * !!! WARNING !!!
+ *
+ * THIS IS AN AUTOGENERATED FILE.
+ * DO NOT EDIT MANUALLY.
+ */
+
+
+
+
+/**
+ * This class holds all the semantic tokens of the Compound theme.
+ */
+@Immutable
+data class SemanticColors(
+ /** Background colour for accent or brand actions. State: Hover */
+ val bgAccentHovered: Color,
+ /** Background colour for accent or brand actions. State: Pressed */
+ val bgAccentPressed: Color,
+ /** Background colour for accent or brand actions. State: Rest. */
+ val bgAccentRest: Color,
+ /** Background colour for accent or brand actions. State: Selected */
+ val bgAccentSelected: Color,
+ /** Background colour for primary actions. State: Disabled. */
+ val bgActionPrimaryDisabled: Color,
+ /** Background colour for primary actions. State: Hover. */
+ val bgActionPrimaryHovered: Color,
+ /** Background colour for primary actions. State: Pressed. */
+ val bgActionPrimaryPressed: Color,
+ /** Background colour for primary actions. State: Rest. */
+ val bgActionPrimaryRest: Color,
+ /** Background colour for secondary actions. State: Hover. */
+ val bgActionSecondaryHovered: Color,
+ /** Background colour for secondary actions. State: Pressed. */
+ val bgActionSecondaryPressed: Color,
+ /** Background colour for secondary actions. State: Rest. */
+ val bgActionSecondaryRest: Color,
+ /** Badge accent background colour */
+ val bgBadgeAccent: Color,
+ /** Badge default background colour */
+ val bgBadgeDefault: Color,
+ /** Badge info background colour */
+ val bgBadgeInfo: Color,
+ /** Default global background for the user interface.
+Elevation: Default (Level 0) */
+ val bgCanvasDefault: Color,
+ /** Default global background for the user interface.
+Elevation: Level 1. */
+ val bgCanvasDefaultLevel1: Color,
+ /** Default background for disabled elements. There's no minimum contrast requirement. */
+ val bgCanvasDisabled: Color,
+ /** High-contrast background color for critical state. State: Hover. */
+ val bgCriticalHovered: Color,
+ /** High-contrast background color for critical state. State: Rest. */
+ val bgCriticalPrimary: Color,
+ /** Default subtle critical surfaces. State: Rest. */
+ val bgCriticalSubtle: Color,
+ /** Default subtle critical surfaces. State: Hover. */
+ val bgCriticalSubtleHovered: Color,
+ /** Decorative background (1, Lime) for avatars and usernames. */
+ val bgDecorative1: Color,
+ /** Decorative background (2, Cyan) for avatars and usernames. */
+ val bgDecorative2: Color,
+ /** Decorative background (3, Fuchsia) for avatars and usernames. */
+ val bgDecorative3: Color,
+ /** Decorative background (4, Purple) for avatars and usernames. */
+ val bgDecorative4: Color,
+ /** Decorative background (5, Pink) for avatars and usernames. */
+ val bgDecorative5: Color,
+ /** Decorative background (6, Orange) for avatars and usernames. */
+ val bgDecorative6: Color,
+ /** Subtle background colour for informational elements. State: Rest. */
+ val bgInfoSubtle: Color,
+ /** Medium contrast surfaces.
+Elevation: Default (Level 2). */
+ val bgSubtlePrimary: Color,
+ /** Low contrast surfaces.
+Elevation: Default (Level 1). */
+ val bgSubtleSecondary: Color,
+ /** Lower contrast surfaces.
+Elevation: Level 0. */
+ val bgSubtleSecondaryLevel0: Color,
+ /** Subtle background colour for success state elements. State: Rest. */
+ val bgSuccessSubtle: Color,
+ /** accent border intended for keylines on message highlights */
+ val borderAccentSubtle: Color,
+ /** High-contrast border for critical state. State: Hover. */
+ val borderCriticalHovered: Color,
+ /** High-contrast border for critical state. State: Rest. */
+ val borderCriticalPrimary: Color,
+ /** Subtle border colour for critical state elements. */
+ val borderCriticalSubtle: Color,
+ /** Used for borders of disabled elements. There's no minimum contrast requirement. */
+ val borderDisabled: Color,
+ /** Used for the focus state outline. */
+ val borderFocused: Color,
+ /** Subtle border colour for informational elements. */
+ val borderInfoSubtle: Color,
+ /** Default contrast for accessible interactive element borders. State: Hover. */
+ val borderInteractiveHovered: Color,
+ /** Default contrast for accessible interactive element borders. State: Rest. */
+ val borderInteractivePrimary: Color,
+ /** ⚠️ Lowest contrast for non-accessible interactive element borders, <3:1. Only use for non-essential borders. Do not rely exclusively on them. State: Rest. */
+ val borderInteractiveSecondary: Color,
+ /** Subtle border colour for success state elements. */
+ val borderSuccessSubtle: Color,
+ /** Background gradient stop for super and send buttons */
+ val gradientActionStop1: Color,
+ /** Background gradient stop for super and send buttons */
+ val gradientActionStop2: Color,
+ /** Background gradient stop for super and send buttons */
+ val gradientActionStop3: Color,
+ /** Background gradient stop for super and send buttons */
+ val gradientActionStop4: Color,
+ /** Subtle background gradient stop for info */
+ val gradientInfoStop1: Color,
+ /** Subtle background gradient stop for info */
+ val gradientInfoStop2: Color,
+ /** Subtle background gradient stop for info */
+ val gradientInfoStop3: Color,
+ /** Subtle background gradient stop for info */
+ val gradientInfoStop4: Color,
+ /** Subtle background gradient stop for info */
+ val gradientInfoStop5: Color,
+ /** Subtle background gradient stop for info */
+ val gradientInfoStop6: Color,
+ /** Subtle background gradient stop for message highlight and bloom */
+ val gradientSubtleStop1: Color,
+ /** Subtle background gradient stop for message highlight and bloom */
+ val gradientSubtleStop2: Color,
+ /** Subtle background gradient stop for message highlight and bloom */
+ val gradientSubtleStop3: Color,
+ /** Subtle background gradient stop for message highlight and bloom */
+ val gradientSubtleStop4: Color,
+ /** Subtle background gradient stop for message highlight and bloom */
+ val gradientSubtleStop5: Color,
+ /** Subtle background gradient stop for message highlight and bloom */
+ val gradientSubtleStop6: Color,
+ /** Highest contrast accessible accent icons. */
+ val iconAccentPrimary: Color,
+ /** Lowest contrast accessible accent icons. */
+ val iconAccentTertiary: Color,
+ /** High-contrast icon for critical state. State: Rest. */
+ val iconCriticalPrimary: Color,
+ /** Use for icons in disabled elements. There's no minimum contrast requirement. */
+ val iconDisabled: Color,
+ /** High-contrast icon for informational elements. */
+ val iconInfoPrimary: Color,
+ /** Highest contrast icon color on top of high-contrast solid backgrounds like primary, accent, or destructive actions. */
+ val iconOnSolidPrimary: Color,
+ /** Highest contrast icons. */
+ val iconPrimary: Color,
+ /** Translucent version of primary icon. Refer to it for intended use. */
+ val iconPrimaryAlpha: Color,
+ /** ⚠️ Lowest contrast non-accessible icons, <3:1. Only use for non-essential icons. Do not rely exclusively on them. */
+ val iconQuaternary: Color,
+ /** Translucent version of quaternary icon. Refer to it for intended use. */
+ val iconQuaternaryAlpha: Color,
+ /** Lower contrast icons. */
+ val iconSecondary: Color,
+ /** Translucent version of secondary icon. Refer to it for intended use. */
+ val iconSecondaryAlpha: Color,
+ /** High-contrast icon for success state elements. */
+ val iconSuccessPrimary: Color,
+ /** Lowest contrast accessible icons. */
+ val iconTertiary: Color,
+ /** Translucent version of tertiary icon. Refer to it for intended use. */
+ val iconTertiaryAlpha: Color,
+ /** Accent text colour for plain actions. */
+ val textActionAccent: Color,
+ /** Default text colour for plain actions. */
+ val textActionPrimary: Color,
+ /** Badge accent text colour */
+ val textBadgeAccent: Color,
+ /** Badge info text colour */
+ val textBadgeInfo: Color,
+ /** Text colour for destructive plain actions. */
+ val textCriticalPrimary: Color,
+ /** Decorative text colour (1, Lime) for avatars and usernames. */
+ val textDecorative1: Color,
+ /** Decorative text colour (2, Cyan) for avatars and usernames. */
+ val textDecorative2: Color,
+ /** Decorative text colour (3, Fuchsia) for avatars and usernames. */
+ val textDecorative3: Color,
+ /** Decorative text colour (4, Purple) for avatars and usernames. */
+ val textDecorative4: Color,
+ /** Decorative text colour (5, Pink) for avatars and usernames. */
+ val textDecorative5: Color,
+ /** Decorative text colour (6, Orange) for avatars and usernames. */
+ val textDecorative6: Color,
+ /** Use for regular text in disabled elements. There's no minimum contrast requirement. */
+ val textDisabled: Color,
+ /** Accent text colour for informational elements. */
+ val textInfoPrimary: Color,
+ /** Text colour for external links. */
+ val textLinkExternal: Color,
+ /** For use as text color on top of high-contrast solid backgrounds like primary, accent, or destructive actions. */
+ val textOnSolidPrimary: Color,
+ /** Highest contrast text. */
+ val textPrimary: Color,
+ /** Lowest contrast text. */
+ val textSecondary: Color,
+ /** Accent text colour for success state elements. */
+ val textSuccessPrimary: Color,
+ /** True for light theme, false for dark theme. */
+ val isLight: Boolean,
+)
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDark.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDark.kt
new file mode 100644
index 0000000000..5ccdfaa308
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDark.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.
+ */
+
+/**
+ * !!! WARNING !!!
+ *
+ * THIS IS AN AUTOGENERATED FILE.
+ * DO NOT EDIT MANUALLY.
+ */
+
+
+
+@file:Suppress("all")
+package io.element.android.compound.tokens.generated
+
+import io.element.android.compound.annotations.CoreColorToken
+import io.element.android.compound.tokens.generated.internal.DarkColorTokens
+
+/**
+ * Semantic colors for the dark Compound theme.
+ */
+@OptIn(CoreColorToken::class)
+val compoundColorsDark = SemanticColors(
+ bgAccentHovered = DarkColorTokens.colorGreen1000,
+ bgAccentPressed = DarkColorTokens.colorGreen1100,
+ bgAccentRest = DarkColorTokens.colorGreen900,
+ bgAccentSelected = DarkColorTokens.colorAlphaGreen300,
+ bgActionPrimaryDisabled = DarkColorTokens.colorGray700,
+ bgActionPrimaryHovered = DarkColorTokens.colorGray1200,
+ bgActionPrimaryPressed = DarkColorTokens.colorGray1100,
+ bgActionPrimaryRest = DarkColorTokens.colorGray1400,
+ bgActionSecondaryHovered = DarkColorTokens.colorAlphaGray200,
+ bgActionSecondaryPressed = DarkColorTokens.colorAlphaGray300,
+ bgActionSecondaryRest = DarkColorTokens.colorThemeBg,
+ bgBadgeAccent = DarkColorTokens.colorAlphaGreen300,
+ bgBadgeDefault = DarkColorTokens.colorAlphaGray300,
+ bgBadgeInfo = DarkColorTokens.colorAlphaBlue300,
+ bgCanvasDefault = DarkColorTokens.colorThemeBg,
+ bgCanvasDefaultLevel1 = DarkColorTokens.colorGray300,
+ bgCanvasDisabled = DarkColorTokens.colorGray200,
+ bgCriticalHovered = DarkColorTokens.colorRed1000,
+ bgCriticalPrimary = DarkColorTokens.colorRed900,
+ bgCriticalSubtle = DarkColorTokens.colorRed200,
+ bgCriticalSubtleHovered = DarkColorTokens.colorRed300,
+ bgDecorative1 = DarkColorTokens.colorLime300,
+ bgDecorative2 = DarkColorTokens.colorCyan300,
+ bgDecorative3 = DarkColorTokens.colorFuchsia300,
+ bgDecorative4 = DarkColorTokens.colorPurple300,
+ bgDecorative5 = DarkColorTokens.colorPink300,
+ bgDecorative6 = DarkColorTokens.colorOrange300,
+ bgInfoSubtle = DarkColorTokens.colorBlue200,
+ bgSubtlePrimary = DarkColorTokens.colorGray400,
+ bgSubtleSecondary = DarkColorTokens.colorGray300,
+ bgSubtleSecondaryLevel0 = DarkColorTokens.colorThemeBg,
+ bgSuccessSubtle = DarkColorTokens.colorGreen200,
+ borderAccentSubtle = DarkColorTokens.colorGreen700,
+ borderCriticalHovered = DarkColorTokens.colorRed1000,
+ borderCriticalPrimary = DarkColorTokens.colorRed900,
+ borderCriticalSubtle = DarkColorTokens.colorRed500,
+ borderDisabled = DarkColorTokens.colorGray500,
+ borderFocused = DarkColorTokens.colorBlue900,
+ borderInfoSubtle = DarkColorTokens.colorBlue700,
+ borderInteractiveHovered = DarkColorTokens.colorGray1100,
+ borderInteractivePrimary = DarkColorTokens.colorGray800,
+ borderInteractiveSecondary = DarkColorTokens.colorGray600,
+ borderSuccessSubtle = DarkColorTokens.colorGreen500,
+ gradientActionStop1 = DarkColorTokens.colorGreen1100,
+ gradientActionStop2 = DarkColorTokens.colorGreen900,
+ gradientActionStop3 = DarkColorTokens.colorGreen700,
+ gradientActionStop4 = DarkColorTokens.colorGreen500,
+ gradientInfoStop1 = DarkColorTokens.colorAlphaBlue500,
+ gradientInfoStop2 = DarkColorTokens.colorAlphaBlue400,
+ gradientInfoStop3 = DarkColorTokens.colorAlphaBlue300,
+ gradientInfoStop4 = DarkColorTokens.colorAlphaBlue200,
+ gradientInfoStop5 = DarkColorTokens.colorAlphaBlue100,
+ gradientInfoStop6 = DarkColorTokens.colorTransparent,
+ gradientSubtleStop1 = DarkColorTokens.colorAlphaGreen500,
+ gradientSubtleStop2 = DarkColorTokens.colorAlphaGreen400,
+ gradientSubtleStop3 = DarkColorTokens.colorAlphaGreen300,
+ gradientSubtleStop4 = DarkColorTokens.colorAlphaGreen200,
+ gradientSubtleStop5 = DarkColorTokens.colorAlphaGreen100,
+ gradientSubtleStop6 = DarkColorTokens.colorTransparent,
+ iconAccentPrimary = DarkColorTokens.colorGreen900,
+ iconAccentTertiary = DarkColorTokens.colorGreen800,
+ iconCriticalPrimary = DarkColorTokens.colorRed900,
+ iconDisabled = DarkColorTokens.colorGray700,
+ iconInfoPrimary = DarkColorTokens.colorBlue900,
+ iconOnSolidPrimary = DarkColorTokens.colorThemeBg,
+ iconPrimary = DarkColorTokens.colorGray1400,
+ iconPrimaryAlpha = DarkColorTokens.colorAlphaGray1400,
+ iconQuaternary = DarkColorTokens.colorGray700,
+ iconQuaternaryAlpha = DarkColorTokens.colorAlphaGray700,
+ iconSecondary = DarkColorTokens.colorGray900,
+ iconSecondaryAlpha = DarkColorTokens.colorAlphaGray900,
+ iconSuccessPrimary = DarkColorTokens.colorGreen900,
+ iconTertiary = DarkColorTokens.colorGray800,
+ iconTertiaryAlpha = DarkColorTokens.colorAlphaGray800,
+ textActionAccent = DarkColorTokens.colorGreen900,
+ textActionPrimary = DarkColorTokens.colorGray1400,
+ textBadgeAccent = DarkColorTokens.colorGreen1100,
+ textBadgeInfo = DarkColorTokens.colorBlue1100,
+ textCriticalPrimary = DarkColorTokens.colorRed900,
+ textDecorative1 = DarkColorTokens.colorLime1100,
+ textDecorative2 = DarkColorTokens.colorCyan1100,
+ textDecorative3 = DarkColorTokens.colorFuchsia1100,
+ textDecorative4 = DarkColorTokens.colorPurple1100,
+ textDecorative5 = DarkColorTokens.colorPink1100,
+ textDecorative6 = DarkColorTokens.colorOrange1100,
+ textDisabled = DarkColorTokens.colorGray800,
+ textInfoPrimary = DarkColorTokens.colorBlue900,
+ textLinkExternal = DarkColorTokens.colorBlue900,
+ textOnSolidPrimary = DarkColorTokens.colorThemeBg,
+ textPrimary = DarkColorTokens.colorGray1400,
+ textSecondary = DarkColorTokens.colorGray900,
+ textSuccessPrimary = DarkColorTokens.colorGreen900,
+ isLight = false,
+)
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDarkHc.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDarkHc.kt
new file mode 100644
index 0000000000..88d2ef3abe
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDarkHc.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.
+ */
+
+/**
+ * !!! WARNING !!!
+ *
+ * THIS IS AN AUTOGENERATED FILE.
+ * DO NOT EDIT MANUALLY.
+ */
+
+
+
+@file:Suppress("all")
+package io.element.android.compound.tokens.generated
+
+import io.element.android.compound.annotations.CoreColorToken
+import io.element.android.compound.tokens.generated.internal.DarkHcColorTokens
+
+/**
+ * Semantic colors for the high contrast dark Compound theme.
+ */
+@OptIn(CoreColorToken::class)
+val compoundColorsHcDark = SemanticColors(
+ bgAccentHovered = DarkHcColorTokens.colorGreen1000,
+ bgAccentPressed = DarkHcColorTokens.colorGreen1100,
+ bgAccentRest = DarkHcColorTokens.colorGreen900,
+ bgAccentSelected = DarkHcColorTokens.colorAlphaGreen300,
+ bgActionPrimaryDisabled = DarkHcColorTokens.colorGray700,
+ bgActionPrimaryHovered = DarkHcColorTokens.colorGray1200,
+ bgActionPrimaryPressed = DarkHcColorTokens.colorGray1100,
+ bgActionPrimaryRest = DarkHcColorTokens.colorGray1400,
+ bgActionSecondaryHovered = DarkHcColorTokens.colorAlphaGray200,
+ bgActionSecondaryPressed = DarkHcColorTokens.colorAlphaGray300,
+ bgActionSecondaryRest = DarkHcColorTokens.colorThemeBg,
+ bgBadgeAccent = DarkHcColorTokens.colorAlphaGreen300,
+ bgBadgeDefault = DarkHcColorTokens.colorAlphaGray300,
+ bgBadgeInfo = DarkHcColorTokens.colorAlphaBlue300,
+ bgCanvasDefault = DarkHcColorTokens.colorThemeBg,
+ bgCanvasDefaultLevel1 = DarkHcColorTokens.colorGray300,
+ bgCanvasDisabled = DarkHcColorTokens.colorGray200,
+ bgCriticalHovered = DarkHcColorTokens.colorRed1000,
+ bgCriticalPrimary = DarkHcColorTokens.colorRed900,
+ bgCriticalSubtle = DarkHcColorTokens.colorRed200,
+ bgCriticalSubtleHovered = DarkHcColorTokens.colorRed300,
+ bgDecorative1 = DarkHcColorTokens.colorLime300,
+ bgDecorative2 = DarkHcColorTokens.colorCyan300,
+ bgDecorative3 = DarkHcColorTokens.colorFuchsia300,
+ bgDecorative4 = DarkHcColorTokens.colorPurple300,
+ bgDecorative5 = DarkHcColorTokens.colorPink300,
+ bgDecorative6 = DarkHcColorTokens.colorOrange300,
+ bgInfoSubtle = DarkHcColorTokens.colorBlue200,
+ bgSubtlePrimary = DarkHcColorTokens.colorGray400,
+ bgSubtleSecondary = DarkHcColorTokens.colorGray300,
+ bgSubtleSecondaryLevel0 = DarkHcColorTokens.colorThemeBg,
+ bgSuccessSubtle = DarkHcColorTokens.colorGreen200,
+ borderAccentSubtle = DarkHcColorTokens.colorGreen700,
+ borderCriticalHovered = DarkHcColorTokens.colorRed1000,
+ borderCriticalPrimary = DarkHcColorTokens.colorRed900,
+ borderCriticalSubtle = DarkHcColorTokens.colorRed500,
+ borderDisabled = DarkHcColorTokens.colorGray500,
+ borderFocused = DarkHcColorTokens.colorBlue900,
+ borderInfoSubtle = DarkHcColorTokens.colorBlue700,
+ borderInteractiveHovered = DarkHcColorTokens.colorGray1100,
+ borderInteractivePrimary = DarkHcColorTokens.colorGray800,
+ borderInteractiveSecondary = DarkHcColorTokens.colorGray600,
+ borderSuccessSubtle = DarkHcColorTokens.colorGreen500,
+ gradientActionStop1 = DarkHcColorTokens.colorGreen1100,
+ gradientActionStop2 = DarkHcColorTokens.colorGreen900,
+ gradientActionStop3 = DarkHcColorTokens.colorGreen700,
+ gradientActionStop4 = DarkHcColorTokens.colorGreen500,
+ gradientInfoStop1 = DarkHcColorTokens.colorAlphaBlue500,
+ gradientInfoStop2 = DarkHcColorTokens.colorAlphaBlue400,
+ gradientInfoStop3 = DarkHcColorTokens.colorAlphaBlue300,
+ gradientInfoStop4 = DarkHcColorTokens.colorAlphaBlue200,
+ gradientInfoStop5 = DarkHcColorTokens.colorAlphaBlue100,
+ gradientInfoStop6 = DarkHcColorTokens.colorTransparent,
+ gradientSubtleStop1 = DarkHcColorTokens.colorAlphaGreen500,
+ gradientSubtleStop2 = DarkHcColorTokens.colorAlphaGreen400,
+ gradientSubtleStop3 = DarkHcColorTokens.colorAlphaGreen300,
+ gradientSubtleStop4 = DarkHcColorTokens.colorAlphaGreen200,
+ gradientSubtleStop5 = DarkHcColorTokens.colorAlphaGreen100,
+ gradientSubtleStop6 = DarkHcColorTokens.colorTransparent,
+ iconAccentPrimary = DarkHcColorTokens.colorGreen900,
+ iconAccentTertiary = DarkHcColorTokens.colorGreen800,
+ iconCriticalPrimary = DarkHcColorTokens.colorRed900,
+ iconDisabled = DarkHcColorTokens.colorGray700,
+ iconInfoPrimary = DarkHcColorTokens.colorBlue900,
+ iconOnSolidPrimary = DarkHcColorTokens.colorThemeBg,
+ iconPrimary = DarkHcColorTokens.colorGray1400,
+ iconPrimaryAlpha = DarkHcColorTokens.colorAlphaGray1400,
+ iconQuaternary = DarkHcColorTokens.colorGray700,
+ iconQuaternaryAlpha = DarkHcColorTokens.colorAlphaGray700,
+ iconSecondary = DarkHcColorTokens.colorGray900,
+ iconSecondaryAlpha = DarkHcColorTokens.colorAlphaGray900,
+ iconSuccessPrimary = DarkHcColorTokens.colorGreen900,
+ iconTertiary = DarkHcColorTokens.colorGray800,
+ iconTertiaryAlpha = DarkHcColorTokens.colorAlphaGray800,
+ textActionAccent = DarkHcColorTokens.colorGreen900,
+ textActionPrimary = DarkHcColorTokens.colorGray1400,
+ textBadgeAccent = DarkHcColorTokens.colorGreen1100,
+ textBadgeInfo = DarkHcColorTokens.colorBlue1100,
+ textCriticalPrimary = DarkHcColorTokens.colorRed900,
+ textDecorative1 = DarkHcColorTokens.colorLime1100,
+ textDecorative2 = DarkHcColorTokens.colorCyan1100,
+ textDecorative3 = DarkHcColorTokens.colorFuchsia1100,
+ textDecorative4 = DarkHcColorTokens.colorPurple1100,
+ textDecorative5 = DarkHcColorTokens.colorPink1100,
+ textDecorative6 = DarkHcColorTokens.colorOrange1100,
+ textDisabled = DarkHcColorTokens.colorGray800,
+ textInfoPrimary = DarkHcColorTokens.colorBlue900,
+ textLinkExternal = DarkHcColorTokens.colorBlue900,
+ textOnSolidPrimary = DarkHcColorTokens.colorThemeBg,
+ textPrimary = DarkHcColorTokens.colorGray1400,
+ textSecondary = DarkHcColorTokens.colorGray900,
+ textSuccessPrimary = DarkHcColorTokens.colorGreen900,
+ isLight = false,
+)
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLight.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLight.kt
new file mode 100644
index 0000000000..03173ad24e
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLight.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.
+ */
+
+/**
+ * !!! WARNING !!!
+ *
+ * THIS IS AN AUTOGENERATED FILE.
+ * DO NOT EDIT MANUALLY.
+ */
+
+
+
+@file:Suppress("all")
+package io.element.android.compound.tokens.generated
+
+import io.element.android.compound.annotations.CoreColorToken
+import io.element.android.compound.tokens.generated.internal.LightColorTokens
+
+/**
+ * Semantic colors for the light Compound theme.
+ */
+@OptIn(CoreColorToken::class)
+val compoundColorsLight = SemanticColors(
+ bgAccentHovered = LightColorTokens.colorGreen1000,
+ bgAccentPressed = LightColorTokens.colorGreen1100,
+ bgAccentRest = LightColorTokens.colorGreen900,
+ bgAccentSelected = LightColorTokens.colorAlphaGreen300,
+ bgActionPrimaryDisabled = LightColorTokens.colorGray700,
+ bgActionPrimaryHovered = LightColorTokens.colorGray1200,
+ bgActionPrimaryPressed = LightColorTokens.colorGray1100,
+ bgActionPrimaryRest = LightColorTokens.colorGray1400,
+ bgActionSecondaryHovered = LightColorTokens.colorAlphaGray200,
+ bgActionSecondaryPressed = LightColorTokens.colorAlphaGray300,
+ bgActionSecondaryRest = LightColorTokens.colorThemeBg,
+ bgBadgeAccent = LightColorTokens.colorAlphaGreen300,
+ bgBadgeDefault = LightColorTokens.colorAlphaGray300,
+ bgBadgeInfo = LightColorTokens.colorAlphaBlue300,
+ bgCanvasDefault = LightColorTokens.colorThemeBg,
+ bgCanvasDefaultLevel1 = LightColorTokens.colorThemeBg,
+ bgCanvasDisabled = LightColorTokens.colorGray200,
+ bgCriticalHovered = LightColorTokens.colorRed1000,
+ bgCriticalPrimary = LightColorTokens.colorRed900,
+ bgCriticalSubtle = LightColorTokens.colorRed200,
+ bgCriticalSubtleHovered = LightColorTokens.colorRed300,
+ bgDecorative1 = LightColorTokens.colorLime300,
+ bgDecorative2 = LightColorTokens.colorCyan300,
+ bgDecorative3 = LightColorTokens.colorFuchsia300,
+ bgDecorative4 = LightColorTokens.colorPurple300,
+ bgDecorative5 = LightColorTokens.colorPink300,
+ bgDecorative6 = LightColorTokens.colorOrange300,
+ bgInfoSubtle = LightColorTokens.colorBlue200,
+ bgSubtlePrimary = LightColorTokens.colorGray400,
+ bgSubtleSecondary = LightColorTokens.colorGray300,
+ bgSubtleSecondaryLevel0 = LightColorTokens.colorGray300,
+ bgSuccessSubtle = LightColorTokens.colorGreen200,
+ borderAccentSubtle = LightColorTokens.colorGreen700,
+ borderCriticalHovered = LightColorTokens.colorRed1000,
+ borderCriticalPrimary = LightColorTokens.colorRed900,
+ borderCriticalSubtle = LightColorTokens.colorRed500,
+ borderDisabled = LightColorTokens.colorGray500,
+ borderFocused = LightColorTokens.colorBlue900,
+ borderInfoSubtle = LightColorTokens.colorBlue700,
+ borderInteractiveHovered = LightColorTokens.colorGray1100,
+ borderInteractivePrimary = LightColorTokens.colorGray800,
+ borderInteractiveSecondary = LightColorTokens.colorGray600,
+ borderSuccessSubtle = LightColorTokens.colorGreen500,
+ gradientActionStop1 = LightColorTokens.colorGreen500,
+ gradientActionStop2 = LightColorTokens.colorGreen700,
+ gradientActionStop3 = LightColorTokens.colorGreen900,
+ gradientActionStop4 = LightColorTokens.colorGreen1100,
+ gradientInfoStop1 = LightColorTokens.colorAlphaBlue500,
+ gradientInfoStop2 = LightColorTokens.colorAlphaBlue400,
+ gradientInfoStop3 = LightColorTokens.colorAlphaBlue300,
+ gradientInfoStop4 = LightColorTokens.colorAlphaBlue200,
+ gradientInfoStop5 = LightColorTokens.colorAlphaBlue100,
+ gradientInfoStop6 = LightColorTokens.colorTransparent,
+ gradientSubtleStop1 = LightColorTokens.colorAlphaGreen500,
+ gradientSubtleStop2 = LightColorTokens.colorAlphaGreen400,
+ gradientSubtleStop3 = LightColorTokens.colorAlphaGreen300,
+ gradientSubtleStop4 = LightColorTokens.colorAlphaGreen200,
+ gradientSubtleStop5 = LightColorTokens.colorAlphaGreen100,
+ gradientSubtleStop6 = LightColorTokens.colorTransparent,
+ iconAccentPrimary = LightColorTokens.colorGreen900,
+ iconAccentTertiary = LightColorTokens.colorGreen800,
+ iconCriticalPrimary = LightColorTokens.colorRed900,
+ iconDisabled = LightColorTokens.colorGray700,
+ iconInfoPrimary = LightColorTokens.colorBlue900,
+ iconOnSolidPrimary = LightColorTokens.colorThemeBg,
+ iconPrimary = LightColorTokens.colorGray1400,
+ iconPrimaryAlpha = LightColorTokens.colorAlphaGray1400,
+ iconQuaternary = LightColorTokens.colorGray700,
+ iconQuaternaryAlpha = LightColorTokens.colorAlphaGray700,
+ iconSecondary = LightColorTokens.colorGray900,
+ iconSecondaryAlpha = LightColorTokens.colorAlphaGray900,
+ iconSuccessPrimary = LightColorTokens.colorGreen900,
+ iconTertiary = LightColorTokens.colorGray800,
+ iconTertiaryAlpha = LightColorTokens.colorAlphaGray800,
+ textActionAccent = LightColorTokens.colorGreen900,
+ textActionPrimary = LightColorTokens.colorGray1400,
+ textBadgeAccent = LightColorTokens.colorGreen1100,
+ textBadgeInfo = LightColorTokens.colorBlue1100,
+ textCriticalPrimary = LightColorTokens.colorRed900,
+ textDecorative1 = LightColorTokens.colorLime1100,
+ textDecorative2 = LightColorTokens.colorCyan1100,
+ textDecorative3 = LightColorTokens.colorFuchsia1100,
+ textDecorative4 = LightColorTokens.colorPurple1100,
+ textDecorative5 = LightColorTokens.colorPink1100,
+ textDecorative6 = LightColorTokens.colorOrange1100,
+ textDisabled = LightColorTokens.colorGray800,
+ textInfoPrimary = LightColorTokens.colorBlue900,
+ textLinkExternal = LightColorTokens.colorBlue900,
+ textOnSolidPrimary = LightColorTokens.colorThemeBg,
+ textPrimary = LightColorTokens.colorGray1400,
+ textSecondary = LightColorTokens.colorGray900,
+ textSuccessPrimary = LightColorTokens.colorGreen900,
+ isLight = true,
+)
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLightHc.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLightHc.kt
new file mode 100644
index 0000000000..00100f7ea8
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLightHc.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.
+ */
+
+/**
+ * !!! WARNING !!!
+ *
+ * THIS IS AN AUTOGENERATED FILE.
+ * DO NOT EDIT MANUALLY.
+ */
+
+
+
+@file:Suppress("all")
+package io.element.android.compound.tokens.generated
+
+import io.element.android.compound.annotations.CoreColorToken
+import io.element.android.compound.tokens.generated.internal.LightHcColorTokens
+
+/**
+ * Semantic colors for the high contrast light Compound theme.
+ */
+@OptIn(CoreColorToken::class)
+val compoundColorsHcLight = SemanticColors(
+ bgAccentHovered = LightHcColorTokens.colorGreen1000,
+ bgAccentPressed = LightHcColorTokens.colorGreen1100,
+ bgAccentRest = LightHcColorTokens.colorGreen900,
+ bgAccentSelected = LightHcColorTokens.colorAlphaGreen300,
+ bgActionPrimaryDisabled = LightHcColorTokens.colorGray700,
+ bgActionPrimaryHovered = LightHcColorTokens.colorGray1200,
+ bgActionPrimaryPressed = LightHcColorTokens.colorGray1100,
+ bgActionPrimaryRest = LightHcColorTokens.colorGray1400,
+ bgActionSecondaryHovered = LightHcColorTokens.colorAlphaGray200,
+ bgActionSecondaryPressed = LightHcColorTokens.colorAlphaGray300,
+ bgActionSecondaryRest = LightHcColorTokens.colorThemeBg,
+ bgBadgeAccent = LightHcColorTokens.colorAlphaGreen300,
+ bgBadgeDefault = LightHcColorTokens.colorAlphaGray300,
+ bgBadgeInfo = LightHcColorTokens.colorAlphaBlue300,
+ bgCanvasDefault = LightHcColorTokens.colorThemeBg,
+ bgCanvasDefaultLevel1 = LightHcColorTokens.colorThemeBg,
+ bgCanvasDisabled = LightHcColorTokens.colorGray200,
+ bgCriticalHovered = LightHcColorTokens.colorRed1000,
+ bgCriticalPrimary = LightHcColorTokens.colorRed900,
+ bgCriticalSubtle = LightHcColorTokens.colorRed200,
+ bgCriticalSubtleHovered = LightHcColorTokens.colorRed300,
+ bgDecorative1 = LightHcColorTokens.colorLime300,
+ bgDecorative2 = LightHcColorTokens.colorCyan300,
+ bgDecorative3 = LightHcColorTokens.colorFuchsia300,
+ bgDecorative4 = LightHcColorTokens.colorPurple300,
+ bgDecorative5 = LightHcColorTokens.colorPink300,
+ bgDecorative6 = LightHcColorTokens.colorOrange300,
+ bgInfoSubtle = LightHcColorTokens.colorBlue200,
+ bgSubtlePrimary = LightHcColorTokens.colorGray400,
+ bgSubtleSecondary = LightHcColorTokens.colorGray300,
+ bgSubtleSecondaryLevel0 = LightHcColorTokens.colorGray300,
+ bgSuccessSubtle = LightHcColorTokens.colorGreen200,
+ borderAccentSubtle = LightHcColorTokens.colorGreen700,
+ borderCriticalHovered = LightHcColorTokens.colorRed1000,
+ borderCriticalPrimary = LightHcColorTokens.colorRed900,
+ borderCriticalSubtle = LightHcColorTokens.colorRed500,
+ borderDisabled = LightHcColorTokens.colorGray500,
+ borderFocused = LightHcColorTokens.colorBlue900,
+ borderInfoSubtle = LightHcColorTokens.colorBlue700,
+ borderInteractiveHovered = LightHcColorTokens.colorGray1100,
+ borderInteractivePrimary = LightHcColorTokens.colorGray800,
+ borderInteractiveSecondary = LightHcColorTokens.colorGray600,
+ borderSuccessSubtle = LightHcColorTokens.colorGreen500,
+ gradientActionStop1 = LightHcColorTokens.colorGreen500,
+ gradientActionStop2 = LightHcColorTokens.colorGreen700,
+ gradientActionStop3 = LightHcColorTokens.colorGreen900,
+ gradientActionStop4 = LightHcColorTokens.colorGreen1100,
+ gradientInfoStop1 = LightHcColorTokens.colorAlphaBlue500,
+ gradientInfoStop2 = LightHcColorTokens.colorAlphaBlue400,
+ gradientInfoStop3 = LightHcColorTokens.colorAlphaBlue300,
+ gradientInfoStop4 = LightHcColorTokens.colorAlphaBlue200,
+ gradientInfoStop5 = LightHcColorTokens.colorAlphaBlue100,
+ gradientInfoStop6 = LightHcColorTokens.colorTransparent,
+ gradientSubtleStop1 = LightHcColorTokens.colorAlphaGreen500,
+ gradientSubtleStop2 = LightHcColorTokens.colorAlphaGreen400,
+ gradientSubtleStop3 = LightHcColorTokens.colorAlphaGreen300,
+ gradientSubtleStop4 = LightHcColorTokens.colorAlphaGreen200,
+ gradientSubtleStop5 = LightHcColorTokens.colorAlphaGreen100,
+ gradientSubtleStop6 = LightHcColorTokens.colorTransparent,
+ iconAccentPrimary = LightHcColorTokens.colorGreen900,
+ iconAccentTertiary = LightHcColorTokens.colorGreen800,
+ iconCriticalPrimary = LightHcColorTokens.colorRed900,
+ iconDisabled = LightHcColorTokens.colorGray700,
+ iconInfoPrimary = LightHcColorTokens.colorBlue900,
+ iconOnSolidPrimary = LightHcColorTokens.colorThemeBg,
+ iconPrimary = LightHcColorTokens.colorGray1400,
+ iconPrimaryAlpha = LightHcColorTokens.colorAlphaGray1400,
+ iconQuaternary = LightHcColorTokens.colorGray700,
+ iconQuaternaryAlpha = LightHcColorTokens.colorAlphaGray700,
+ iconSecondary = LightHcColorTokens.colorGray900,
+ iconSecondaryAlpha = LightHcColorTokens.colorAlphaGray900,
+ iconSuccessPrimary = LightHcColorTokens.colorGreen900,
+ iconTertiary = LightHcColorTokens.colorGray800,
+ iconTertiaryAlpha = LightHcColorTokens.colorAlphaGray800,
+ textActionAccent = LightHcColorTokens.colorGreen900,
+ textActionPrimary = LightHcColorTokens.colorGray1400,
+ textBadgeAccent = LightHcColorTokens.colorGreen1100,
+ textBadgeInfo = LightHcColorTokens.colorBlue1100,
+ textCriticalPrimary = LightHcColorTokens.colorRed900,
+ textDecorative1 = LightHcColorTokens.colorLime1100,
+ textDecorative2 = LightHcColorTokens.colorCyan1100,
+ textDecorative3 = LightHcColorTokens.colorFuchsia1100,
+ textDecorative4 = LightHcColorTokens.colorPurple1100,
+ textDecorative5 = LightHcColorTokens.colorPink1100,
+ textDecorative6 = LightHcColorTokens.colorOrange1100,
+ textDisabled = LightHcColorTokens.colorGray800,
+ textInfoPrimary = LightHcColorTokens.colorBlue900,
+ textLinkExternal = LightHcColorTokens.colorBlue900,
+ textOnSolidPrimary = LightHcColorTokens.colorThemeBg,
+ textPrimary = LightHcColorTokens.colorGray1400,
+ textSecondary = LightHcColorTokens.colorGray900,
+ textSuccessPrimary = LightHcColorTokens.colorGreen900,
+ isLight = true,
+)
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/TypographyTokens.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/TypographyTokens.kt
new file mode 100644
index 0000000000..a25b79234c
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/TypographyTokens.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+
+/**
+ * !!! WARNING !!!
+ *
+ * THIS IS AN AUTOGENERATED FILE.
+ * DO NOT EDIT MANUALLY.
+ */
+
+
+
+@file:Suppress("all")
+package io.element.android.compound.tokens.generated
+
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.text.PlatformTextStyle
+import androidx.compose.ui.text.style.LineHeightStyle
+
+object TypographyTokens {
+ val fontBodyLgMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W500,
+ lineHeight = 22.sp,
+ fontSize = 16.sp,
+ letterSpacing = 0.015625.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontBodyLgRegular = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W400,
+ lineHeight = 22.sp,
+ fontSize = 16.sp,
+ letterSpacing = 0.015625.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontBodyMdMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W500,
+ lineHeight = 20.sp,
+ fontSize = 14.sp,
+ letterSpacing = 0.017857.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontBodyMdRegular = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W400,
+ lineHeight = 20.sp,
+ fontSize = 14.sp,
+ letterSpacing = 0.017857.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontBodySmMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W500,
+ lineHeight = 17.sp,
+ fontSize = 12.sp,
+ letterSpacing = 0.033333.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontBodySmRegular = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W400,
+ lineHeight = 17.sp,
+ fontSize = 12.sp,
+ letterSpacing = 0.033333.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontBodyXsMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W500,
+ lineHeight = 15.sp,
+ fontSize = 11.sp,
+ letterSpacing = 0.045454.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontBodyXsRegular = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W400,
+ lineHeight = 15.sp,
+ fontSize = 11.sp,
+ letterSpacing = 0.045454.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontHeadingLgBold = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W700,
+ lineHeight = 34.sp,
+ fontSize = 28.sp,
+ letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontHeadingLgRegular = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W400,
+ lineHeight = 34.sp,
+ fontSize = 28.sp,
+ letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontHeadingMdBold = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W700,
+ lineHeight = 27.sp,
+ fontSize = 22.sp,
+ letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontHeadingMdRegular = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W400,
+ lineHeight = 27.sp,
+ fontSize = 22.sp,
+ letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontHeadingSmMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W500,
+ lineHeight = 25.sp,
+ fontSize = 20.sp,
+ letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontHeadingSmRegular = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W400,
+ lineHeight = 25.sp,
+ fontSize = 20.sp,
+ letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontHeadingXlBold = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W700,
+ lineHeight = 41.sp,
+ fontSize = 34.sp,
+ letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+ val fontHeadingXlRegular = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W400,
+ lineHeight = 41.sp,
+ fontSize = 34.sp,
+ letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
+ )
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/internal/DarkColorTokens.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/internal/DarkColorTokens.kt
new file mode 100644
index 0000000000..1272c0df16
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/internal/DarkColorTokens.kt
@@ -0,0 +1,335 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+/**
+ * !!! WARNING !!!
+ *
+ * THIS IS AN AUTOGENERATED FILE.
+ * DO NOT EDIT MANUALLY.
+ */
+
+
+
+@file:Suppress("all")
+package io.element.android.compound.tokens.generated.internal
+
+import androidx.compose.ui.graphics.Color
+import io.element.android.compound.annotations.CoreColorToken
+
+@CoreColorToken
+object DarkColorTokens {
+ val colorAlphaBlue100 = Color(0xff00055c)
+ val colorAlphaBlue1000 = Color(0xf062a0fe)
+ val colorAlphaBlue1100 = Color(0xf57cb2fd)
+ val colorAlphaBlue1200 = Color(0xf7a3c8ff)
+ val colorAlphaBlue1300 = Color(0xfccde1fe)
+ val colorAlphaBlue1400 = Color(0xffe6effe)
+ val colorAlphaBlue200 = Color(0xff00095c)
+ val colorAlphaBlue300 = Color(0xff001366)
+ val colorAlphaBlue400 = Color(0xff001e70)
+ val colorAlphaBlue500 = Color(0xa1003cbd)
+ val colorAlphaBlue600 = Color(0x87015afe)
+ val colorAlphaBlue700 = Color(0xa30665fe)
+ val colorAlphaBlue800 = Color(0xd61077fe)
+ val colorAlphaBlue900 = Color(0xeb4491fd)
+ val colorAlphaCyan100 = Color(0xff001142)
+ val colorAlphaCyan1000 = Color(0xe000bfe0)
+ val colorAlphaCyan1100 = Color(0xc926e7fd)
+ val colorAlphaCyan1200 = Color(0xd98af1ff)
+ val colorAlphaCyan1300 = Color(0xebc9f7fd)
+ val colorAlphaCyan1400 = Color(0xf5e1fbfe)
+ val colorAlphaCyan200 = Color(0xff001447)
+ val colorAlphaCyan300 = Color(0xff001b4d)
+ val colorAlphaCyan400 = Color(0xff00265c)
+ val colorAlphaCyan500 = Color(0xff003366)
+ val colorAlphaCyan600 = Color(0xff003f75)
+ val colorAlphaCyan700 = Color(0xff00538a)
+ val colorAlphaCyan800 = Color(0xe0007ebd)
+ val colorAlphaCyan900 = Color(0xff0091bd)
+ val colorAlphaFuchsia100 = Color(0xff28003d)
+ val colorAlphaFuchsia1000 = Color(0xd4f790fe)
+ val colorAlphaFuchsia1100 = Color(0xdbfaa4fe)
+ val colorAlphaFuchsia1200 = Color(0xe8fac3fe)
+ val colorAlphaFuchsia1300 = Color(0xf2fde0ff)
+ val colorAlphaFuchsia1400 = Color(0xfafdecfe)
+ val colorAlphaFuchsia200 = Color(0xff2d0042)
+ val colorAlphaFuchsia300 = Color(0xff36004d)
+ val colorAlphaFuchsia400 = Color(0xff45005c)
+ val colorAlphaFuchsia500 = Color(0x61ca0aff)
+ val colorAlphaFuchsia600 = Color(0x70d21fff)
+ val colorAlphaFuchsia700 = Color(0x8ad82ffe)
+ val colorAlphaFuchsia800 = Color(0xb5eb44fd)
+ val colorAlphaFuchsia900 = Color(0xccf172fd)
+ val colorAlphaGray100 = Color(0x05d8dbdf)
+ val colorAlphaGray1000 = Color(0x9ce1eefe)
+ val colorAlphaGray1100 = Color(0xade7f0fe)
+ val colorAlphaGray1200 = Color(0xc9edf4fc)
+ val colorAlphaGray1300 = Color(0xe3f2f7fd)
+ val colorAlphaGray1400 = Color(0xf2f6f9fe)
+ val colorAlphaGray200 = Color(0x0ad9c3df)
+ val colorAlphaGray300 = Color(0x0fe9dbf0)
+ val colorAlphaGray400 = Color(0x1aede7f4)
+ val colorAlphaGray500 = Color(0x26f4f7fa)
+ val colorAlphaGray600 = Color(0x33eceff8)
+ val colorAlphaGray700 = Color(0x45e7f1fd)
+ val colorAlphaGray800 = Color(0x69e0edff)
+ val colorAlphaGray900 = Color(0x8ae1effe)
+ val colorAlphaGreen100 = Color(0xff001f0c)
+ val colorAlphaGreen1000 = Color(0xa61bfebd)
+ val colorAlphaGreen1100 = Color(0xbd26fdbc)
+ val colorAlphaGreen1200 = Color(0xd486fdce)
+ val colorAlphaGreen1300 = Color(0xe8c4fde2)
+ val colorAlphaGreen1400 = Color(0xf5e2fdf1)
+ val colorAlphaGreen200 = Color(0xff001f0e)
+ val colorAlphaGreen300 = Color(0xff002412)
+ val colorAlphaGreen400 = Color(0xff002e1b)
+ val colorAlphaGreen500 = Color(0xff003d29)
+ val colorAlphaGreen600 = Color(0xff004732)
+ val colorAlphaGreen700 = Color(0xff005c45)
+ val colorAlphaGreen800 = Color(0xff007a62)
+ val colorAlphaGreen900 = Color(0x9412fdbe)
+ val colorAlphaLime100 = Color(0xff001a00)
+ val colorAlphaLime1000 = Color(0xa860fc2c)
+ val colorAlphaLime1100 = Color(0xbd71fd35)
+ val colorAlphaLime1200 = Color(0xd68dff5c)
+ val colorAlphaLime1300 = Color(0xebc3ffad)
+ val colorAlphaLime1400 = Color(0xf7e1fdd8)
+ val colorAlphaLime200 = Color(0xff001f00)
+ val colorAlphaLime300 = Color(0xff002900)
+ val colorAlphaLime400 = Color(0xff002e00)
+ val colorAlphaLime500 = Color(0xff003d00)
+ val colorAlphaLime600 = Color(0xff004d00)
+ val colorAlphaLime700 = Color(0xff005c00)
+ val colorAlphaLime800 = Color(0x732dfd0d)
+ val colorAlphaLime900 = Color(0x9454fd26)
+ val colorAlphaOrange100 = Color(0xff380000)
+ val colorAlphaOrange1000 = Color(0xebfe8310)
+ val colorAlphaOrange1100 = Color(0xf7fd953f)
+ val colorAlphaOrange1200 = Color(0xfcfdb781)
+ val colorAlphaOrange1300 = Color(0xffffd4b8)
+ val colorAlphaOrange1400 = Color(0xffffeadb)
+ val colorAlphaOrange200 = Color(0xff3d0000)
+ val colorAlphaOrange300 = Color(0xff470000)
+ val colorAlphaOrange400 = Color(0xff570000)
+ val colorAlphaOrange500 = Color(0xff700000)
+ val colorAlphaOrange600 = Color(0xff850400)
+ val colorAlphaOrange700 = Color(0xbdc72800)
+ val colorAlphaOrange800 = Color(0xb5ff5900)
+ val colorAlphaOrange900 = Color(0xd9fe740b)
+ val colorAlphaPink100 = Color(0xff38000f)
+ val colorAlphaPink1000 = Color(0xfaff6691)
+ val colorAlphaPink1100 = Color(0xfffe86a4)
+ val colorAlphaPink1200 = Color(0xffffadc0)
+ val colorAlphaPink1300 = Color(0xffffd1db)
+ val colorAlphaPink1400 = Color(0xffffebef)
+ val colorAlphaPink200 = Color(0xff3d0012)
+ val colorAlphaPink300 = Color(0xff470019)
+ val colorAlphaPink400 = Color(0xff570024)
+ val colorAlphaPink500 = Color(0xff6b0036)
+ val colorAlphaPink600 = Color(0x75fb0473)
+ val colorAlphaPink700 = Color(0x94fd1277)
+ val colorAlphaPink800 = Color(0xccfe1b79)
+ val colorAlphaPink900 = Color(0xf5fe4382)
+ val colorAlphaPurple100 = Color(0xff1a0057)
+ val colorAlphaPurple1000 = Color(0xfca28bfe)
+ val colorAlphaPurple1100 = Color(0xffab9afe)
+ val colorAlphaPurple1200 = Color(0xffc7bdff)
+ val colorAlphaPurple1300 = Color(0xffdfdbff)
+ val colorAlphaPurple1400 = Color(0xffeeebff)
+ val colorAlphaPurple200 = Color(0xff1d005c)
+ val colorAlphaPurple300 = Color(0xff22006b)
+ val colorAlphaPurple400 = Color(0xff2d0080)
+ val colorAlphaPurple500 = Color(0xff3d009e)
+ val colorAlphaPurple600 = Color(0xab690dfd)
+ val colorAlphaPurple700 = Color(0xc2712bfd)
+ val colorAlphaPurple800 = Color(0xeb7f4dff)
+ val colorAlphaPurple900 = Color(0xfa9271fe)
+ val colorAlphaRed100 = Color(0xff380000)
+ val colorAlphaRed1000 = Color(0xffff645c)
+ val colorAlphaRed1100 = Color(0xffff857a)
+ val colorAlphaRed1200 = Color(0xffffaea3)
+ val colorAlphaRed1300 = Color(0xffffd3cc)
+ val colorAlphaRed1400 = Color(0xffffe8e5)
+ val colorAlphaRed200 = Color(0xff3d0000)
+ val colorAlphaRed300 = Color(0xff470000)
+ val colorAlphaRed400 = Color(0xff5c0000)
+ val colorAlphaRed500 = Color(0xff700000)
+ val colorAlphaRed600 = Color(0xff850009)
+ val colorAlphaRed700 = Color(0x99fe0b24)
+ val colorAlphaRed800 = Color(0xcffe2530)
+ val colorAlphaRed900 = Color(0xfffd3d3a)
+ val colorAlphaYellow100 = Color(0xff380000)
+ val colorAlphaYellow1000 = Color(0xffcc8b00)
+ val colorAlphaYellow1100 = Color(0xffdba100)
+ val colorAlphaYellow1200 = Color(0xf0fdc50d)
+ val colorAlphaYellow1300 = Color(0xfffeda58)
+ val colorAlphaYellow1400 = Color(0xffffedb3)
+ val colorAlphaYellow200 = Color(0xff380300)
+ val colorAlphaYellow300 = Color(0xff420900)
+ val colorAlphaYellow400 = Color(0xff4d1400)
+ val colorAlphaYellow500 = Color(0xff5c2300)
+ val colorAlphaYellow600 = Color(0xde753300)
+ val colorAlphaYellow700 = Color(0xeb854200)
+ val colorAlphaYellow800 = Color(0xff9e5c00)
+ val colorAlphaYellow900 = Color(0xffbd7b00)
+ val colorBlue100 = Color(0xff00055a)
+ val colorBlue1000 = Color(0xff5e99f0)
+ val colorBlue1100 = Color(0xff7aacf4)
+ val colorBlue1200 = Color(0xffa1c4f8)
+ val colorBlue1300 = Color(0xffcbdffc)
+ val colorBlue1400 = Color(0xffe4eefe)
+ val colorBlue200 = Color(0xff00095d)
+ val colorBlue300 = Color(0xff001264)
+ val colorBlue400 = Color(0xff001e6f)
+ val colorBlue500 = Color(0xff062d80)
+ val colorBlue600 = Color(0xff083891)
+ val colorBlue700 = Color(0xff0b49ab)
+ val colorBlue800 = Color(0xff0e67d9)
+ val colorBlue900 = Color(0xff4187eb)
+ val colorCyan100 = Color(0xff001144)
+ val colorCyan1000 = Color(0xff02a7c6)
+ val colorCyan1100 = Color(0xff21bacd)
+ val colorCyan1200 = Color(0xff78d0dc)
+ val colorCyan1300 = Color(0xffb8e5eb)
+ val colorCyan1400 = Color(0xffdbf2f5)
+ val colorCyan200 = Color(0xff001448)
+ val colorCyan300 = Color(0xff001b4e)
+ val colorCyan400 = Color(0xff002559)
+ val colorCyan500 = Color(0xff003468)
+ val colorCyan600 = Color(0xff003f75)
+ val colorCyan700 = Color(0xff005188)
+ val colorCyan800 = Color(0xff0271aa)
+ val colorCyan900 = Color(0xff0093be)
+ val colorFuchsia100 = Color(0xff28003d)
+ val colorFuchsia1000 = Color(0xffcf78d7)
+ val colorFuchsia1100 = Color(0xffd991de)
+ val colorFuchsia1200 = Color(0xffe5b1e9)
+ val colorFuchsia1300 = Color(0xfff1d4f3)
+ val colorFuchsia1400 = Color(0xfff8e9f9)
+ val colorFuchsia200 = Color(0xff2e0044)
+ val colorFuchsia300 = Color(0xff37004e)
+ val colorFuchsia400 = Color(0xff46005e)
+ val colorFuchsia500 = Color(0xff560f6f)
+ val colorFuchsia600 = Color(0xff65177d)
+ val colorFuchsia700 = Color(0xff7d2394)
+ val colorFuchsia800 = Color(0xffaa36ba)
+ val colorFuchsia900 = Color(0xffc560cf)
+ val colorGray100 = Color(0xff14171b)
+ val colorGray1000 = Color(0xff9199a4)
+ val colorGray1100 = Color(0xffa3aab4)
+ val colorGray1200 = Color(0xffbdc3cc)
+ val colorGray1300 = Color(0xffd9dee4)
+ val colorGray1400 = Color(0xffebeef2)
+ val colorGray200 = Color(0xff181a1f)
+ val colorGray300 = Color(0xff1d1f24)
+ val colorGray400 = Color(0xff26282d)
+ val colorGray500 = Color(0xff323539)
+ val colorGray600 = Color(0xff3c3f44)
+ val colorGray700 = Color(0xff4a4f55)
+ val colorGray800 = Color(0xff656c76)
+ val colorGray900 = Color(0xff808994)
+ val colorGreen100 = Color(0xff001c0b)
+ val colorGreen1000 = Color(0xff17ac84)
+ val colorGreen1100 = Color(0xff1fc090)
+ val colorGreen1200 = Color(0xff72d5ae)
+ val colorGreen1300 = Color(0xffb5e8d1)
+ val colorGreen1400 = Color(0xffd9f4e7)
+ val colorGreen200 = Color(0xff001f0e)
+ val colorGreen300 = Color(0xff002513)
+ val colorGreen400 = Color(0xff002e1b)
+ val colorGreen500 = Color(0xff003d29)
+ val colorGreen600 = Color(0xff004832)
+ val colorGreen700 = Color(0xff005a43)
+ val colorGreen800 = Color(0xff007a62)
+ val colorGreen900 = Color(0xff129a78)
+ val colorLime100 = Color(0xff001b00)
+ val colorLime1000 = Color(0xff47ad26)
+ val colorLime1100 = Color(0xff56c02c)
+ val colorLime1200 = Color(0xff77d94f)
+ val colorLime1300 = Color(0xffb6eca3)
+ val colorLime1400 = Color(0xffdaf6d0)
+ val colorLime200 = Color(0xff002000)
+ val colorLime300 = Color(0xff002600)
+ val colorLime400 = Color(0xff003000)
+ val colorLime500 = Color(0xff003e00)
+ val colorLime600 = Color(0xff004a00)
+ val colorLime700 = Color(0xff005c00)
+ val colorLime800 = Color(0xff1d7c13)
+ val colorLime900 = Color(0xff389b20)
+ val colorOrange100 = Color(0xff380000)
+ val colorOrange1000 = Color(0xffeb7a12)
+ val colorOrange1100 = Color(0xfff6913d)
+ val colorOrange1200 = Color(0xfffbb37e)
+ val colorOrange1300 = Color(0xffffd5b9)
+ val colorOrange1400 = Color(0xffffeadb)
+ val colorOrange200 = Color(0xff3c0000)
+ val colorOrange300 = Color(0xff470000)
+ val colorOrange400 = Color(0xff580000)
+ val colorOrange500 = Color(0xff710000)
+ val colorOrange600 = Color(0xff830500)
+ val colorOrange700 = Color(0xff972206)
+ val colorOrange800 = Color(0xffb94607)
+ val colorOrange900 = Color(0xffda670d)
+ val colorPink100 = Color(0xff37000f)
+ val colorPink1000 = Color(0xfffa658f)
+ val colorPink1100 = Color(0xfffe84a2)
+ val colorPink1200 = Color(0xffffabbe)
+ val colorPink1300 = Color(0xffffd2dc)
+ val colorPink1400 = Color(0xffffe8ed)
+ val colorPink200 = Color(0xff3c0012)
+ val colorPink300 = Color(0xff450018)
+ val colorPink400 = Color(0xff550024)
+ val colorPink500 = Color(0xff6d0036)
+ val colorPink600 = Color(0xff7c0c41)
+ val colorPink700 = Color(0xff99114f)
+ val colorPink800 = Color(0xffce1865)
+ val colorPink900 = Color(0xfff4427d)
+ val colorPurple100 = Color(0xff1a0055)
+ val colorPurple1000 = Color(0xff9e87fc)
+ val colorPurple1100 = Color(0xffad9cfe)
+ val colorPurple1200 = Color(0xffc4baff)
+ val colorPurple1300 = Color(0xffdedaff)
+ val colorPurple1400 = Color(0xffeeebff)
+ val colorPurple200 = Color(0xff1c005a)
+ val colorPurple300 = Color(0xff22006a)
+ val colorPurple400 = Color(0xff2c0080)
+ val colorPurple500 = Color(0xff3d009e)
+ val colorPurple600 = Color(0xff4a0db1)
+ val colorPurple700 = Color(0xff5a27c6)
+ val colorPurple800 = Color(0xff7849ec)
+ val colorPurple900 = Color(0xff9171f9)
+ val colorRed100 = Color(0xff370000)
+ val colorRed1000 = Color(0xffff665d)
+ val colorRed1100 = Color(0xffff877c)
+ val colorRed1200 = Color(0xffffaea4)
+ val colorRed1300 = Color(0xffffd4cd)
+ val colorRed1400 = Color(0xffffe9e6)
+ val colorRed200 = Color(0xff3e0000)
+ val colorRed300 = Color(0xff470000)
+ val colorRed400 = Color(0xff590000)
+ val colorRed500 = Color(0xff710000)
+ val colorRed600 = Color(0xff830009)
+ val colorRed700 = Color(0xff9f0d1e)
+ val colorRed800 = Color(0xffd1212a)
+ val colorRed900 = Color(0xfffd3e3c)
+ val colorThemeBg = Color(0xff101317)
+ val colorTransparent = Color(0x00000000)
+ val colorYellow100 = Color(0xff360000)
+ val colorYellow1000 = Color(0xffcc8c00)
+ val colorYellow1100 = Color(0xffdb9f00)
+ val colorYellow1200 = Color(0xffefbb0b)
+ val colorYellow1300 = Color(0xfffedb58)
+ val colorYellow1400 = Color(0xffffedb1)
+ val colorYellow200 = Color(0xff3a0300)
+ val colorYellow300 = Color(0xff410900)
+ val colorYellow400 = Color(0xff4c1400)
+ val colorYellow500 = Color(0xff5c2400)
+ val colorYellow600 = Color(0xff682e03)
+ val colorYellow700 = Color(0xff7c3e02)
+ val colorYellow800 = Color(0xff9d5b00)
+ val colorYellow900 = Color(0xffbc7a00)
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/internal/DarkHcColorTokens.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/internal/DarkHcColorTokens.kt
new file mode 100644
index 0000000000..31406f6ffc
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/internal/DarkHcColorTokens.kt
@@ -0,0 +1,335 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+/**
+ * !!! WARNING !!!
+ *
+ * THIS IS AN AUTOGENERATED FILE.
+ * DO NOT EDIT MANUALLY.
+ */
+
+
+
+@file:Suppress("all")
+package io.element.android.compound.tokens.generated.internal
+
+import androidx.compose.ui.graphics.Color
+import io.element.android.compound.annotations.CoreColorToken
+
+@CoreColorToken
+object DarkHcColorTokens {
+ val colorAlphaBlue100 = Color(0xff00095c)
+ val colorAlphaBlue1000 = Color(0xf79ec5ff)
+ val colorAlphaBlue1100 = Color(0xfab8d4ff)
+ val colorAlphaBlue1200 = Color(0xfcc8defe)
+ val colorAlphaBlue1300 = Color(0xffe6effe)
+ val colorAlphaBlue1400 = Color(0xfff1f6fe)
+ val colorAlphaBlue200 = Color(0xff001366)
+ val colorAlphaBlue300 = Color(0xff001e70)
+ val colorAlphaBlue400 = Color(0xd1002b8f)
+ val colorAlphaBlue500 = Color(0x87015afe)
+ val colorAlphaBlue600 = Color(0xa30665fe)
+ val colorAlphaBlue700 = Color(0xcf0d71fd)
+ val colorAlphaBlue800 = Color(0xe83488fe)
+ val colorAlphaBlue900 = Color(0xf78bb9fd)
+ val colorAlphaCyan100 = Color(0xff001447)
+ val colorAlphaCyan1000 = Color(0xd67beffe)
+ val colorAlphaCyan1100 = Color(0xe0a4f4fe)
+ val colorAlphaCyan1200 = Color(0xe8bef5fe)
+ val colorAlphaCyan1300 = Color(0xf5e1fbfe)
+ val colorAlphaCyan1400 = Color(0xfaf1fdfe)
+ val colorAlphaCyan200 = Color(0xff001b4d)
+ val colorAlphaCyan300 = Color(0xff00265c)
+ val colorAlphaCyan400 = Color(0xff002d61)
+ val colorAlphaCyan500 = Color(0xff003f75)
+ val colorAlphaCyan600 = Color(0xff00538a)
+ val colorAlphaCyan700 = Color(0xff006da3)
+ val colorAlphaCyan800 = Color(0xff008ebd)
+ val colorAlphaCyan900 = Color(0xcf52edfe)
+ val colorAlphaFuchsia100 = Color(0xff2d0042)
+ val colorAlphaFuchsia1000 = Color(0xe6fabefe)
+ val colorAlphaFuchsia1100 = Color(0xedfacefd)
+ val colorAlphaFuchsia1200 = Color(0xf2fcd7fe)
+ val colorAlphaFuchsia1300 = Color(0xfafdecfe)
+ val colorAlphaFuchsia1400 = Color(0xfcfdf2fd)
+ val colorAlphaFuchsia200 = Color(0xff36004d)
+ val colorAlphaFuchsia300 = Color(0xff45005c)
+ val colorAlphaFuchsia400 = Color(0xd95a0075)
+ val colorAlphaFuchsia500 = Color(0x70d21fff)
+ val colorAlphaFuchsia600 = Color(0x8ad82ffe)
+ val colorAlphaFuchsia700 = Color(0xade640fc)
+ val colorAlphaFuchsia800 = Color(0xc7f467fe)
+ val colorAlphaFuchsia900 = Color(0xe0f9b3ff)
+ val colorAlphaGray100 = Color(0x0ad9c3df)
+ val colorAlphaGray1000 = Color(0xc2f0f7ff)
+ val colorAlphaGray1100 = Color(0xd1f0f7ff)
+ val colorAlphaGray1200 = Color(0xe0f1f6fd)
+ val colorAlphaGray1300 = Color(0xf2f6f9fe)
+ val colorAlphaGray1400 = Color(0xf7fbfdfe)
+ val colorAlphaGray200 = Color(0x0fe9dbf0)
+ val colorAlphaGray300 = Color(0x1aede7f4)
+ val colorAlphaGray400 = Color(0x21e1e4ef)
+ val colorAlphaGray500 = Color(0x33eceff8)
+ val colorAlphaGray600 = Color(0x45e7f1fd)
+ val colorAlphaGray700 = Color(0x63dfebfb)
+ val colorAlphaGray800 = Color(0x82dceafe)
+ val colorAlphaGray900 = Color(0xb8ecf4fe)
+ val colorAlphaGreen100 = Color(0xff001f0e)
+ val colorAlphaGreen1000 = Color(0xcf75ffc8)
+ val colorAlphaGreen1100 = Color(0xdba4fed7)
+ val colorAlphaGreen1200 = Color(0xe6bffde1)
+ val colorAlphaGreen1300 = Color(0xf5e2fdf1)
+ val colorAlphaGreen1400 = Color(0xfaedfdf5)
+ val colorAlphaGreen200 = Color(0xff002412)
+ val colorAlphaGreen300 = Color(0xff002e1b)
+ val colorAlphaGreen400 = Color(0xff003824)
+ val colorAlphaGreen500 = Color(0xff004732)
+ val colorAlphaGreen600 = Color(0xff005c45)
+ val colorAlphaGreen700 = Color(0xff00755e)
+ val colorAlphaGreen800 = Color(0x8a12fdc2)
+ val colorAlphaGreen900 = Color(0xc740fcba)
+ val colorAlphaLime100 = Color(0xff001f00)
+ val colorAlphaLime1000 = Color(0xd47bfe3e)
+ val colorAlphaLime1100 = Color(0xe0a4fd81)
+ val colorAlphaLime1200 = Color(0xe8c1fea9)
+ val colorAlphaLime1300 = Color(0xf7e1fdd8)
+ val colorAlphaLime1400 = Color(0xfaedfee7)
+ val colorAlphaLime200 = Color(0xff002900)
+ val colorAlphaLime300 = Color(0xff002e00)
+ val colorAlphaLime400 = Color(0xff003800)
+ val colorAlphaLime500 = Color(0xff004d00)
+ val colorAlphaLime600 = Color(0xff005c00)
+ val colorAlphaLime700 = Color(0x6b23ff0a)
+ val colorAlphaLime800 = Color(0x8c4dfe25)
+ val colorAlphaLime900 = Color(0xc774fe34)
+ val colorAlphaOrange100 = Color(0xff3d0000)
+ val colorAlphaOrange1000 = Color(0xfaffb175)
+ val colorAlphaOrange1100 = Color(0xfffdc196)
+ val colorAlphaOrange1200 = Color(0xfffed1b3)
+ val colorAlphaOrange1300 = Color(0xffffeadb)
+ val colorAlphaOrange1400 = Color(0xfffff2eb)
+ val colorAlphaOrange200 = Color(0xff470000)
+ val colorAlphaOrange300 = Color(0xff570000)
+ val colorAlphaOrange400 = Color(0xff660000)
+ val colorAlphaOrange500 = Color(0xff850400)
+ val colorAlphaOrange600 = Color(0xbdc72800)
+ val colorAlphaOrange700 = Color(0xb3fa5300)
+ val colorAlphaOrange800 = Color(0xcffe7206)
+ val colorAlphaOrange900 = Color(0xfafda058)
+ val colorAlphaPink100 = Color(0xff3d0012)
+ val colorAlphaPink1000 = Color(0xffffa3b9)
+ val colorAlphaPink1100 = Color(0xffffbdcb)
+ val colorAlphaPink1200 = Color(0xffffccd7)
+ val colorAlphaPink1300 = Color(0xffffebef)
+ val colorAlphaPink1400 = Color(0xfffff0f3)
+ val colorAlphaPink200 = Color(0xff470019)
+ val colorAlphaPink300 = Color(0xff570024)
+ val colorAlphaPink400 = Color(0xff61002d)
+ val colorAlphaPink500 = Color(0x75fb0473)
+ val colorAlphaPink600 = Color(0x94fd1277)
+ val colorAlphaPink700 = Color(0xc2fe1b79)
+ val colorAlphaPink800 = Color(0xf2fd2b78)
+ val colorAlphaPink900 = Color(0xffff94ad)
+ val colorAlphaPurple100 = Color(0xff1d005c)
+ val colorAlphaPurple1000 = Color(0xffc2b8ff)
+ val colorAlphaPurple1100 = Color(0xffcec7ff)
+ val colorAlphaPurple1200 = Color(0xffdbd6ff)
+ val colorAlphaPurple1300 = Color(0xffeeebff)
+ val colorAlphaPurple1400 = Color(0xfff6f5ff)
+ val colorAlphaPurple200 = Color(0xff22006b)
+ val colorAlphaPurple300 = Color(0xff2d0080)
+ val colorAlphaPurple400 = Color(0xff34008f)
+ val colorAlphaPurple500 = Color(0xab690dfd)
+ val colorAlphaPurple600 = Color(0xc2712bfd)
+ val colorAlphaPurple700 = Color(0xe67f49fd)
+ val colorAlphaPurple800 = Color(0xf7906bff)
+ val colorAlphaPurple900 = Color(0xffb7a8ff)
+ val colorAlphaRed100 = Color(0xff3d0000)
+ val colorAlphaRed1000 = Color(0xffffa89e)
+ val colorAlphaRed1100 = Color(0xffffbfb8)
+ val colorAlphaRed1200 = Color(0xffffcec7)
+ val colorAlphaRed1300 = Color(0xffffe8e5)
+ val colorAlphaRed1400 = Color(0xfffff3f0)
+ val colorAlphaRed200 = Color(0xff470000)
+ val colorAlphaRed300 = Color(0xff5c0000)
+ val colorAlphaRed400 = Color(0xff660000)
+ val colorAlphaRed500 = Color(0xff850009)
+ val colorAlphaRed600 = Color(0x99fe0b24)
+ val colorAlphaRed700 = Color(0xc4ff242f)
+ val colorAlphaRed800 = Color(0xf5ff2e31)
+ val colorAlphaRed900 = Color(0xffff988f)
+ val colorAlphaYellow100 = Color(0xff380300)
+ val colorAlphaYellow1000 = Color(0xebfec406)
+ val colorAlphaYellow1100 = Color(0xf7fecf16)
+ val colorAlphaYellow1200 = Color(0xfffed634)
+ val colorAlphaYellow1300 = Color(0xffffedb3)
+ val colorAlphaYellow1400 = Color(0xfffff4d1)
+ val colorAlphaYellow200 = Color(0xff420900)
+ val colorAlphaYellow300 = Color(0xff4d1400)
+ val colorAlphaYellow400 = Color(0xff571e00)
+ val colorAlphaYellow500 = Color(0xde753300)
+ val colorAlphaYellow600 = Color(0xeb854200)
+ val colorAlphaYellow700 = Color(0xff995700)
+ val colorAlphaYellow800 = Color(0xffb37100)
+ val colorAlphaYellow900 = Color(0xffe6ac00)
+ val colorBlue100 = Color(0xff00095d)
+ val colorBlue1000 = Color(0xff9ac0f8)
+ val colorBlue1100 = Color(0xffb2cffa)
+ val colorBlue1200 = Color(0xffc5dbfc)
+ val colorBlue1300 = Color(0xffe4eefe)
+ val colorBlue1400 = Color(0xffeff5fe)
+ val colorBlue200 = Color(0xff001264)
+ val colorBlue300 = Color(0xff001e6f)
+ val colorBlue400 = Color(0xff032677)
+ val colorBlue500 = Color(0xff083891)
+ val colorBlue600 = Color(0xff0b49ab)
+ val colorBlue700 = Color(0xff0e61d1)
+ val colorBlue800 = Color(0xff337fe9)
+ val colorBlue900 = Color(0xff89b5f6)
+ val colorCyan100 = Color(0xff001448)
+ val colorCyan1000 = Color(0xff6bccd9)
+ val colorCyan1100 = Color(0xff93d9e2)
+ val colorCyan1200 = Color(0xffafe2e9)
+ val colorCyan1300 = Color(0xffdbf2f5)
+ val colorCyan1400 = Color(0xffeaf7f9)
+ val colorCyan200 = Color(0xff001b4e)
+ val colorCyan300 = Color(0xff002559)
+ val colorCyan400 = Color(0xff002d61)
+ val colorCyan500 = Color(0xff003f75)
+ val colorCyan600 = Color(0xff005188)
+ val colorCyan700 = Color(0xff006ca4)
+ val colorCyan800 = Color(0xff008aba)
+ val colorCyan900 = Color(0xff46c3d2)
+ val colorFuchsia100 = Color(0xff2e0044)
+ val colorFuchsia1000 = Color(0xffe3abe7)
+ val colorFuchsia1100 = Color(0xffeac0ed)
+ val colorFuchsia1200 = Color(0xfff0cff2)
+ val colorFuchsia1300 = Color(0xfff8e9f9)
+ val colorFuchsia1400 = Color(0xfffbf1fb)
+ val colorFuchsia200 = Color(0xff37004e)
+ val colorFuchsia300 = Color(0xff46005e)
+ val colorFuchsia400 = Color(0xff4f0368)
+ val colorFuchsia500 = Color(0xff65177d)
+ val colorFuchsia600 = Color(0xff7d2394)
+ val colorFuchsia700 = Color(0xffa233b3)
+ val colorFuchsia800 = Color(0xffc153cb)
+ val colorFuchsia900 = Color(0xffdd9de3)
+ val colorGray100 = Color(0xff181a1f)
+ val colorGray1000 = Color(0xffb8bfc7)
+ val colorGray1100 = Color(0xffc8ced5)
+ val colorGray1200 = Color(0xffd5dae1)
+ val colorGray1300 = Color(0xffebeef2)
+ val colorGray1400 = Color(0xfff2f5f7)
+ val colorGray200 = Color(0xff1d1f24)
+ val colorGray300 = Color(0xff26282d)
+ val colorGray400 = Color(0xff2b2e33)
+ val colorGray500 = Color(0xff3c3f44)
+ val colorGray600 = Color(0xff4a4f55)
+ val colorGray700 = Color(0xff606770)
+ val colorGray800 = Color(0xff79818d)
+ val colorGray900 = Color(0xffacb4bd)
+ val colorGreen100 = Color(0xff001f0e)
+ val colorGreen1000 = Color(0xff61d2a6)
+ val colorGreen1100 = Color(0xff8fddbc)
+ val colorGreen1200 = Color(0xfface6cc)
+ val colorGreen1300 = Color(0xffd9f4e7)
+ val colorGreen1400 = Color(0xffe9f8f1)
+ val colorGreen200 = Color(0xff002513)
+ val colorGreen300 = Color(0xff002e1b)
+ val colorGreen400 = Color(0xff003622)
+ val colorGreen500 = Color(0xff004832)
+ val colorGreen600 = Color(0xff005a43)
+ val colorGreen700 = Color(0xff00745c)
+ val colorGreen800 = Color(0xff109173)
+ val colorGreen900 = Color(0xff37c998)
+ val colorLime100 = Color(0xff002000)
+ val colorLime1000 = Color(0xff6ad639)
+ val colorLime1100 = Color(0xff92e175)
+ val colorLime1200 = Color(0xffafe99a)
+ val colorLime1300 = Color(0xffdaf6d0)
+ val colorLime1400 = Color(0xffe9f9e3)
+ val colorLime200 = Color(0xff002600)
+ val colorLime300 = Color(0xff003000)
+ val colorLime400 = Color(0xff003700)
+ val colorLime500 = Color(0xff004a00)
+ val colorLime600 = Color(0xff005c00)
+ val colorLime700 = Color(0xff187611)
+ val colorLime800 = Color(0xff31941d)
+ val colorLime900 = Color(0xff5eca2f)
+ val colorOrange100 = Color(0xff3c0000)
+ val colorOrange1000 = Color(0xfffaad73)
+ val colorOrange1100 = Color(0xfffdc197)
+ val colorOrange1200 = Color(0xfffed0b1)
+ val colorOrange1300 = Color(0xffffeadb)
+ val colorOrange1400 = Color(0xfffff2ea)
+ val colorOrange200 = Color(0xff470000)
+ val colorOrange300 = Color(0xff580000)
+ val colorOrange400 = Color(0xff650000)
+ val colorOrange500 = Color(0xff830500)
+ val colorOrange600 = Color(0xff972206)
+ val colorOrange700 = Color(0xffb44007)
+ val colorOrange800 = Color(0xffd15f0b)
+ val colorOrange900 = Color(0xfff89d58)
+ val colorPink100 = Color(0xff3c0012)
+ val colorPink1000 = Color(0xffffa4b9)
+ val colorPink1100 = Color(0xffffbbca)
+ val colorPink1200 = Color(0xffffccd7)
+ val colorPink1300 = Color(0xffffe8ed)
+ val colorPink1400 = Color(0xfffff1f4)
+ val colorPink200 = Color(0xff450018)
+ val colorPink300 = Color(0xff550024)
+ val colorPink400 = Color(0xff61002d)
+ val colorPink500 = Color(0xff7c0c41)
+ val colorPink600 = Color(0xff99114f)
+ val colorPink700 = Color(0xffc51761)
+ val colorPink800 = Color(0xfff12c75)
+ val colorPink900 = Color(0xffff92ac)
+ val colorPurple100 = Color(0xff1c005a)
+ val colorPurple1000 = Color(0xffc0b5ff)
+ val colorPurple1100 = Color(0xffcec7ff)
+ val colorPurple1200 = Color(0xffdad5ff)
+ val colorPurple1300 = Color(0xffeeebff)
+ val colorPurple1400 = Color(0xfff5f3ff)
+ val colorPurple200 = Color(0xff22006a)
+ val colorPurple300 = Color(0xff2c0080)
+ val colorPurple400 = Color(0xff350090)
+ val colorPurple500 = Color(0xff4a0db1)
+ val colorPurple600 = Color(0xff5a27c6)
+ val colorPurple700 = Color(0xff7343e6)
+ val colorPurple800 = Color(0xff8b66f8)
+ val colorPurple900 = Color(0xffb6a7ff)
+ val colorRed100 = Color(0xff3e0000)
+ val colorRed1000 = Color(0xffffa79d)
+ val colorRed1100 = Color(0xffffbdb5)
+ val colorRed1200 = Color(0xffffcfc8)
+ val colorRed1300 = Color(0xffffe9e6)
+ val colorRed1400 = Color(0xfffff2ef)
+ val colorRed200 = Color(0xff470000)
+ val colorRed300 = Color(0xff590000)
+ val colorRed400 = Color(0xff640000)
+ val colorRed500 = Color(0xff830009)
+ val colorRed600 = Color(0xff9f0d1e)
+ val colorRed700 = Color(0xffc81e28)
+ val colorRed800 = Color(0xfff52f33)
+ val colorRed900 = Color(0xffff968c)
+ val colorThemeBg = Color(0xff101317)
+ val colorTransparent = Color(0x00000000)
+ val colorYellow100 = Color(0xff3a0300)
+ val colorYellow1000 = Color(0xffebb607)
+ val colorYellow1100 = Color(0xfff7c816)
+ val colorYellow1200 = Color(0xfffed632)
+ val colorYellow1300 = Color(0xffffedb1)
+ val colorYellow1400 = Color(0xfffff4d0)
+ val colorYellow200 = Color(0xff410900)
+ val colorYellow300 = Color(0xff4c1400)
+ val colorYellow400 = Color(0xff541d00)
+ val colorYellow500 = Color(0xff682e03)
+ val colorYellow600 = Color(0xff7c3e02)
+ val colorYellow700 = Color(0xff985600)
+ val colorYellow800 = Color(0xffb47200)
+ val colorYellow900 = Color(0xffe3aa00)
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/internal/LightColorTokens.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/internal/LightColorTokens.kt
new file mode 100644
index 0000000000..119db9dade
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/internal/LightColorTokens.kt
@@ -0,0 +1,335 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+/**
+ * !!! WARNING !!!
+ *
+ * THIS IS AN AUTOGENERATED FILE.
+ * DO NOT EDIT MANUALLY.
+ */
+
+
+
+@file:Suppress("all")
+package io.element.android.compound.tokens.generated.internal
+
+import androidx.compose.ui.graphics.Color
+import io.element.android.compound.annotations.CoreColorToken
+
+@CoreColorToken
+object LightColorTokens {
+ val colorAlphaBlue100 = Color(0x08389cff)
+ val colorAlphaBlue1000 = Color(0xfc0256c5)
+ val colorAlphaBlue1100 = Color(0xfa0148b2)
+ val colorAlphaBlue1200 = Color(0xfc013693)
+ val colorAlphaBlue1300 = Color(0xff012579)
+ val colorAlphaBlue1400 = Color(0xff000e66)
+ val colorAlphaBlue200 = Color(0x0d2474ff)
+ val colorAlphaBlue300 = Color(0x170a70ff)
+ val colorAlphaBlue400 = Color(0x290b6af9)
+ val colorAlphaBlue500 = Color(0x47096cf6)
+ val colorAlphaBlue600 = Color(0x5e0663ef)
+ val colorAlphaBlue700 = Color(0x820264ed)
+ val colorAlphaBlue800 = Color(0xbf0062eb)
+ val colorAlphaBlue900 = Color(0xfc0165df)
+ val colorAlphaCyan100 = Color(0x0816bbbb)
+ val colorAlphaCyan1000 = Color(0xff00649e)
+ val colorAlphaCyan1100 = Color(0xff00568f)
+ val colorAlphaCyan1200 = Color(0xff003f75)
+ val colorAlphaCyan1300 = Color(0xff002c61)
+ val colorAlphaCyan1400 = Color(0xff001a52)
+ val colorAlphaCyan200 = Color(0x0f16abbb)
+ val colorAlphaCyan300 = Color(0x1c00a8c2)
+ val colorAlphaCyan400 = Color(0x3800aabd)
+ val colorAlphaCyan500 = Color(0x6605abbd)
+ val colorAlphaCyan600 = Color(0x8a01aac1)
+ val colorAlphaCyan700 = Color(0xeb01b7cb)
+ val colorAlphaCyan800 = Color(0xff0095c2)
+ val colorAlphaCyan900 = Color(0xff0074ad)
+ val colorAlphaFuchsia100 = Color(0x05cc05cc)
+ val colorAlphaFuchsia1000 = Color(0xd6820198)
+ val colorAlphaFuchsia1100 = Color(0xe073038c)
+ val colorAlphaFuchsia1200 = Color(0xed5d0279)
+ val colorAlphaFuchsia1300 = Color(0xff4d0066)
+ val colorAlphaFuchsia1400 = Color(0xff34004d)
+ val colorAlphaFuchsia200 = Color(0x0ab505cc)
+ val colorAlphaFuchsia300 = Color(0x12b60cc6)
+ val colorAlphaFuchsia400 = Color(0x21bd09c3)
+ val colorAlphaFuchsia500 = Color(0x3bb407c0)
+ val colorAlphaFuchsia600 = Color(0x4fb207bb)
+ val colorAlphaFuchsia700 = Color(0x6eaa04b9)
+ val colorAlphaFuchsia800 = Color(0xa3ab03ba)
+ val colorAlphaFuchsia900 = Color(0xcc9900ad)
+ val colorAlphaGray100 = Color(0x0536699b)
+ val colorAlphaGray1000 = Color(0xa8030c1b)
+ val colorAlphaGray1100 = Color(0xb5030b16)
+ val colorAlphaGray1200 = Color(0xc402070d)
+ val colorAlphaGray1300 = Color(0xd603050c)
+ val colorAlphaGray1400 = Color(0xe6020408)
+ val colorAlphaGray200 = Color(0x0a366881)
+ val colorAlphaGray300 = Color(0x0f052657)
+ val colorAlphaGray400 = Color(0x1f052e61)
+ val colorAlphaGray500 = Color(0x33052448)
+ val colorAlphaGray600 = Color(0x42011d3c)
+ val colorAlphaGray700 = Color(0x59011532)
+ val colorAlphaGray800 = Color(0x8003152b)
+ val colorAlphaGray900 = Color(0x9c031021)
+ val colorAlphaGreen100 = Color(0x0816bb79)
+ val colorAlphaGreen1000 = Color(0xff006b52)
+ val colorAlphaGreen1100 = Color(0xff005c45)
+ val colorAlphaGreen1200 = Color(0xff004732)
+ val colorAlphaGreen1300 = Color(0xff00331f)
+ val colorAlphaGreen1400 = Color(0xff002411)
+ val colorAlphaGreen200 = Color(0x0f16bb69)
+ val colorAlphaGreen300 = Color(0x1c00b85c)
+ val colorAlphaGreen400 = Color(0x3b07b661)
+ val colorAlphaGreen500 = Color(0x6904b96a)
+ val colorAlphaGreen600 = Color(0x8f01b76e)
+ val colorAlphaGreen700 = Color(0xf501c18a)
+ val colorAlphaGreen800 = Color(0xff009975)
+ val colorAlphaGreen900 = Color(0xff007a62)
+ val colorAlphaLime100 = Color(0x0a4fcd1d)
+ val colorAlphaLime1000 = Color(0xff007000)
+ val colorAlphaLime1100 = Color(0xff006100)
+ val colorAlphaLime1200 = Color(0xff004d00)
+ val colorAlphaLime1300 = Color(0xff003800)
+ val colorAlphaLime1400 = Color(0xff002400)
+ val colorAlphaLime200 = Color(0x1238d40c)
+ val colorAlphaLime300 = Color(0x262ecf02)
+ val colorAlphaLime400 = Color(0x473ace09)
+ val colorAlphaLime500 = Color(0x8237ca02)
+ val colorAlphaLime600 = Color(0xb540ce03)
+ val colorAlphaLime700 = Color(0xdb39bd00)
+ val colorAlphaLime800 = Color(0xe8209301)
+ val colorAlphaLime900 = Color(0xf5107902)
+ val colorAlphaOrange100 = Color(0x0aff8138)
+ val colorAlphaOrange1000 = Color(0xffad3400)
+ val colorAlphaOrange1100 = Color(0xff992100)
+ val colorAlphaOrange1200 = Color(0xff850000)
+ val colorAlphaOrange1300 = Color(0xff610000)
+ val colorAlphaOrange1400 = Color(0xff470000)
+ val colorAlphaOrange200 = Color(0x12ff7d1a)
+ val colorAlphaOrange300 = Color(0x1cff6c0a)
+ val colorAlphaOrange400 = Color(0x38ff6d05)
+ val colorAlphaOrange500 = Color(0x5eff6a00)
+ val colorAlphaOrange600 = Color(0x85fc6f03)
+ val colorAlphaOrange700 = Color(0xbff56e00)
+ val colorAlphaOrange800 = Color(0xffdb6600)
+ val colorAlphaOrange900 = Color(0xffbd4500)
+ val colorAlphaPink100 = Color(0x05ff0537)
+ val colorAlphaPink1000 = Color(0xf7b60256)
+ val colorAlphaPink1100 = Color(0xf79e004c)
+ val colorAlphaPink1200 = Color(0xfa79013d)
+ val colorAlphaPink1300 = Color(0xff61002c)
+ val colorAlphaPink1400 = Color(0xff420017)
+ val colorAlphaPink200 = Color(0x0aff0537)
+ val colorAlphaPink300 = Color(0x14ff1447)
+ val colorAlphaPink400 = Color(0x21ff0037)
+ val colorAlphaPink500 = Color(0x3dff0037)
+ val colorAlphaPink600 = Color(0x54ff053f)
+ val colorAlphaPink700 = Color(0x78ff0040)
+ val colorAlphaPink800 = Color(0xbff50052)
+ val colorAlphaPink900 = Color(0xf5cf025e)
+ val colorAlphaPurple100 = Color(0x053838ff)
+ val colorAlphaPurple1000 = Color(0xc94502d4)
+ val colorAlphaPurple1100 = Color(0xdb4303c4)
+ val colorAlphaPurple1200 = Color(0xfc4a02b6)
+ val colorAlphaPurple1300 = Color(0xff34008f)
+ val colorAlphaPurple1400 = Color(0xff200066)
+ val colorAlphaPurple200 = Color(0x0a5338ff)
+ val colorAlphaPurple300 = Color(0x12381aff)
+ val colorAlphaPurple400 = Color(0x1f2f0fff)
+ val colorAlphaPurple500 = Color(0x332605ff)
+ val colorAlphaPurple600 = Color(0x452b05ff)
+ val colorAlphaPurple700 = Color(0x613305ff)
+ val colorAlphaPurple800 = Color(0x8f3b01f9)
+ val colorAlphaPurple900 = Color(0xba4902ed)
+ val colorAlphaRed100 = Color(0x08ff5938)
+ val colorAlphaRed1000 = Color(0xf2bb0217)
+ val colorAlphaRed1100 = Color(0xfca2011c)
+ val colorAlphaRed1200 = Color(0xff850007)
+ val colorAlphaRed1300 = Color(0xff610000)
+ val colorAlphaRed1400 = Color(0xff470000)
+ val colorAlphaRed200 = Color(0x0aff391f)
+ val colorAlphaRed300 = Color(0x14ff3814)
+ val colorAlphaRed400 = Color(0x26ff2b0a)
+ val colorAlphaRed500 = Color(0x45ff2605)
+ val colorAlphaRed600 = Color(0x5cff2205)
+ val colorAlphaRed700 = Color(0x80ff1a05)
+ val colorAlphaRed800 = Color(0xc4ff0505)
+ val colorAlphaRed900 = Color(0xe8cf0213)
+ val colorAlphaYellow100 = Color(0x0fffcd05)
+ val colorAlphaYellow1000 = Color(0xff8f4c00)
+ val colorAlphaYellow1100 = Color(0xff804000)
+ val colorAlphaYellow1200 = Color(0xff6b2e00)
+ val colorAlphaYellow1300 = Color(0xff571b00)
+ val colorAlphaYellow1400 = Color(0xff420700)
+ val colorAlphaYellow200 = Color(0x21ffc70f)
+ val colorAlphaYellow300 = Color(0x40ffc905)
+ val colorAlphaYellow400 = Color(0x7dffc905)
+ val colorAlphaYellow500 = Color(0xfffacc00)
+ val colorAlphaYellow600 = Color(0xfff0bc00)
+ val colorAlphaYellow700 = Color(0xffe0a500)
+ val colorAlphaYellow800 = Color(0xffbd7b00)
+ val colorAlphaYellow900 = Color(0xff9e5a00)
+ val colorBlue100 = Color(0xfff9fcff)
+ val colorBlue1000 = Color(0xff0558c7)
+ val colorBlue1100 = Color(0xff064ab1)
+ val colorBlue1200 = Color(0xff043894)
+ val colorBlue1300 = Color(0xff012478)
+ val colorBlue1400 = Color(0xff000e65)
+ val colorBlue200 = Color(0xfff4f8ff)
+ val colorBlue300 = Color(0xffe9f2ff)
+ val colorBlue400 = Color(0xffd8e7fe)
+ val colorBlue500 = Color(0xffbad5fc)
+ val colorBlue600 = Color(0xffa3c6fa)
+ val colorBlue700 = Color(0xff7eaff6)
+ val colorBlue800 = Color(0xff4088ee)
+ val colorBlue900 = Color(0xff0467dd)
+ val colorCyan100 = Color(0xfff8fdfd)
+ val colorCyan1000 = Color(0xff00629c)
+ val colorCyan1100 = Color(0xff00548c)
+ val colorCyan1200 = Color(0xff004077)
+ val colorCyan1300 = Color(0xff002b61)
+ val colorCyan1400 = Color(0xff00194f)
+ val colorCyan200 = Color(0xfff1fafb)
+ val colorCyan300 = Color(0xffe3f5f8)
+ val colorCyan400 = Color(0xffc7ecf0)
+ val colorCyan500 = Color(0xff9bdde5)
+ val colorCyan600 = Color(0xff76d1dd)
+ val colorCyan700 = Color(0xff15becf)
+ val colorCyan800 = Color(0xff0094c0)
+ val colorCyan900 = Color(0xff0072ac)
+ val colorFuchsia100 = Color(0xfffefafe)
+ val colorFuchsia1000 = Color(0xff972aaa)
+ val colorFuchsia1100 = Color(0xff822198)
+ val colorFuchsia1200 = Color(0xff671481)
+ val colorFuchsia1300 = Color(0xff4e0068)
+ val colorFuchsia1400 = Color(0xff34004c)
+ val colorFuchsia200 = Color(0xfffcf5fd)
+ val colorFuchsia300 = Color(0xfffaeefb)
+ val colorFuchsia400 = Color(0xfff6dff7)
+ val colorFuchsia500 = Color(0xffedc6f0)
+ val colorFuchsia600 = Color(0xffe7b2ea)
+ val colorFuchsia700 = Color(0xffdb93e1)
+ val colorFuchsia800 = Color(0xffc85ed1)
+ val colorFuchsia900 = Color(0xffad33bd)
+ val colorGray100 = Color(0xfffbfcfd)
+ val colorGray1000 = Color(0xff595e67)
+ val colorGray1100 = Color(0xff4c5158)
+ val colorGray1200 = Color(0xff3c4045)
+ val colorGray1300 = Color(0xff2b2d32)
+ val colorGray1400 = Color(0xff1b1d22)
+ val colorGray200 = Color(0xfff7f9fa)
+ val colorGray300 = Color(0xfff0f2f5)
+ val colorGray400 = Color(0xffe1e6ec)
+ val colorGray500 = Color(0xffcdd3da)
+ val colorGray600 = Color(0xffbdc4cc)
+ val colorGray700 = Color(0xffa6adb7)
+ val colorGray800 = Color(0xff818a95)
+ val colorGray900 = Color(0xff656d77)
+ val colorGreen100 = Color(0xfff8fdfb)
+ val colorGreen1000 = Color(0xff006b52)
+ val colorGreen1100 = Color(0xff005c45)
+ val colorGreen1200 = Color(0xff004933)
+ val colorGreen1300 = Color(0xff003420)
+ val colorGreen1400 = Color(0xff002311)
+ val colorGreen200 = Color(0xfff1fbf6)
+ val colorGreen300 = Color(0xffe3f7ed)
+ val colorGreen400 = Color(0xffc6eedb)
+ val colorGreen500 = Color(0xff98e1c1)
+ val colorGreen600 = Color(0xff71d7ae)
+ val colorGreen700 = Color(0xff0bc491)
+ val colorGreen800 = Color(0xff009b78)
+ val colorGreen900 = Color(0xff007a61)
+ val colorLime100 = Color(0xfff8fdf6)
+ val colorLime1000 = Color(0xff006e00)
+ val colorLime1100 = Color(0xff005f00)
+ val colorLime1200 = Color(0xff004b00)
+ val colorLime1300 = Color(0xff003600)
+ val colorLime1400 = Color(0xff002400)
+ val colorLime200 = Color(0xfff1fcee)
+ val colorLime300 = Color(0xffe0f8d9)
+ val colorLime400 = Color(0xffc8f1ba)
+ val colorLime500 = Color(0xff99e57e)
+ val colorLime600 = Color(0xff76db4c)
+ val colorLime700 = Color(0xff54c424)
+ val colorLime800 = Color(0xff359d18)
+ val colorLime900 = Color(0xff197d0c)
+ val colorOrange100 = Color(0xfffffaf7)
+ val colorOrange1000 = Color(0xffac3300)
+ val colorOrange1100 = Color(0xff9b2200)
+ val colorOrange1200 = Color(0xff850000)
+ val colorOrange1300 = Color(0xff620000)
+ val colorOrange1400 = Color(0xff450000)
+ val colorOrange200 = Color(0xfffff6ef)
+ val colorOrange300 = Color(0xffffefe4)
+ val colorOrange400 = Color(0xffffdfc8)
+ val colorOrange500 = Color(0xffffc8a1)
+ val colorOrange600 = Color(0xfffdb37c)
+ val colorOrange700 = Color(0xfff89440)
+ val colorOrange800 = Color(0xffdc6700)
+ val colorOrange900 = Color(0xffbc4500)
+ val colorPink100 = Color(0xfffffafb)
+ val colorPink1000 = Color(0xffb80a5b)
+ val colorPink1100 = Color(0xff9f0850)
+ val colorPink1200 = Color(0xff7e0642)
+ val colorPink1300 = Color(0xff5f002b)
+ val colorPink1400 = Color(0xff430017)
+ val colorPink200 = Color(0xfffff5f7)
+ val colorPink300 = Color(0xffffecf0)
+ val colorPink400 = Color(0xffffdee5)
+ val colorPink500 = Color(0xffffc2cf)
+ val colorPink600 = Color(0xffffadc0)
+ val colorPink700 = Color(0xffff88a6)
+ val colorPink800 = Color(0xfff7407d)
+ val colorPink900 = Color(0xffd20c65)
+ val colorPurple100 = Color(0xfffbfbff)
+ val colorPurple1000 = Color(0xff6b37de)
+ val colorPurple1100 = Color(0xff5d26cd)
+ val colorPurple1200 = Color(0xff4c05b5)
+ val colorPurple1300 = Color(0xff33008d)
+ val colorPurple1400 = Color(0xff200066)
+ val colorPurple200 = Color(0xfff8f7ff)
+ val colorPurple300 = Color(0xfff1efff)
+ val colorPurple400 = Color(0xffe6e2ff)
+ val colorPurple500 = Color(0xffd4cdff)
+ val colorPurple600 = Color(0xffc5bbff)
+ val colorPurple700 = Color(0xffb1a0ff)
+ val colorPurple800 = Color(0xff9271fd)
+ val colorPurple900 = Color(0xff7a47f1)
+ val colorRed100 = Color(0xfffffaf9)
+ val colorRed1000 = Color(0xffbc0f22)
+ val colorRed1100 = Color(0xffa4041d)
+ val colorRed1200 = Color(0xff850006)
+ val colorRed1300 = Color(0xff620000)
+ val colorRed1400 = Color(0xff450000)
+ val colorRed200 = Color(0xfffff7f6)
+ val colorRed300 = Color(0xffffefec)
+ val colorRed400 = Color(0xffffdfda)
+ val colorRed500 = Color(0xffffc5bc)
+ val colorRed600 = Color(0xffffafa5)
+ val colorRed700 = Color(0xffff8c81)
+ val colorRed800 = Color(0xffff3d3d)
+ val colorRed900 = Color(0xffd51928)
+ val colorThemeBg = Color(0xffffffff)
+ val colorTransparent = Color(0x00000000)
+ val colorYellow100 = Color(0xfffffcf0)
+ val colorYellow1000 = Color(0xff8f4d00)
+ val colorYellow1100 = Color(0xff803f00)
+ val colorYellow1200 = Color(0xff692e00)
+ val colorYellow1300 = Color(0xff541a00)
+ val colorYellow1400 = Color(0xff410600)
+ val colorYellow200 = Color(0xfffff8e0)
+ val colorYellow300 = Color(0xfffff2c1)
+ val colorYellow400 = Color(0xffffe484)
+ val colorYellow500 = Color(0xfffbce00)
+ val colorYellow600 = Color(0xfff1bd00)
+ val colorYellow700 = Color(0xffdea200)
+ val colorYellow800 = Color(0xffbe7a00)
+ val colorYellow900 = Color(0xff9f5b00)
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/internal/LightHcColorTokens.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/internal/LightHcColorTokens.kt
new file mode 100644
index 0000000000..86624bb597
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/internal/LightHcColorTokens.kt
@@ -0,0 +1,335 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+/**
+ * !!! WARNING !!!
+ *
+ * THIS IS AN AUTOGENERATED FILE.
+ * DO NOT EDIT MANUALLY.
+ */
+
+
+
+@file:Suppress("all")
+package io.element.android.compound.tokens.generated.internal
+
+import androidx.compose.ui.graphics.Color
+import io.element.android.compound.annotations.CoreColorToken
+
+@CoreColorToken
+object LightHcColorTokens {
+ val colorAlphaBlue100 = Color(0x0d2474ff)
+ val colorAlphaBlue1000 = Color(0xfc023997)
+ val colorAlphaBlue1100 = Color(0xfc012e89)
+ val colorAlphaBlue1200 = Color(0xfc00257a)
+ val colorAlphaBlue1300 = Color(0xff00156b)
+ val colorAlphaBlue1400 = Color(0xff000b61)
+ val colorAlphaBlue200 = Color(0x170a70ff)
+ val colorAlphaBlue300 = Color(0x290b6af9)
+ val colorAlphaBlue400 = Color(0x380565f5)
+ val colorAlphaBlue500 = Color(0x5e0663ef)
+ val colorAlphaBlue600 = Color(0x820264ed)
+ val colorAlphaBlue700 = Color(0xb50062eb)
+ val colorAlphaBlue800 = Color(0xfc016ee9)
+ val colorAlphaBlue900 = Color(0xfc0241a7)
+ val colorAlphaCyan100 = Color(0x0f16abbb)
+ val colorAlphaCyan1000 = Color(0xff00437a)
+ val colorAlphaCyan1100 = Color(0xff003870)
+ val colorAlphaCyan1200 = Color(0xff003066)
+ val colorAlphaCyan1300 = Color(0xff001e52)
+ val colorAlphaCyan1400 = Color(0xff00174d)
+ val colorAlphaCyan200 = Color(0x1c00a8c2)
+ val colorAlphaCyan300 = Color(0x3800aabd)
+ val colorAlphaCyan400 = Color(0x4f03a9bf)
+ val colorAlphaCyan500 = Color(0x8a01aac1)
+ val colorAlphaCyan600 = Color(0xeb01b7cb)
+ val colorAlphaCyan700 = Color(0xff0098c2)
+ val colorAlphaCyan800 = Color(0xff007ab3)
+ val colorAlphaCyan900 = Color(0xff004d85)
+ val colorAlphaFuchsia100 = Color(0x0ab505cc)
+ val colorAlphaFuchsia1000 = Color(0xe85e007a)
+ val colorAlphaFuchsia1100 = Color(0xf253026f)
+ val colorAlphaFuchsia1200 = Color(0xff53026e)
+ val colorAlphaFuchsia1300 = Color(0xff3a0052)
+ val colorAlphaFuchsia1400 = Color(0xff34004d)
+ val colorAlphaFuchsia200 = Color(0x12b60cc6)
+ val colorAlphaFuchsia300 = Color(0x21bd09c3)
+ val colorAlphaFuchsia400 = Color(0x2eb105bd)
+ val colorAlphaFuchsia500 = Color(0x4fb207bb)
+ val colorAlphaFuchsia600 = Color(0x6eaa04b9)
+ val colorAlphaFuchsia700 = Color(0x99ab03ba)
+ val colorAlphaFuchsia800 = Color(0xc9a402b6)
+ val colorAlphaFuchsia900 = Color(0xe66a0387)
+ val colorAlphaGray100 = Color(0x0a366881)
+ val colorAlphaGray1000 = Color(0xc202060d)
+ val colorAlphaGray1100 = Color(0xcc03060c)
+ val colorAlphaGray1200 = Color(0xd4020509)
+ val colorAlphaGray1300 = Color(0xe000040a)
+ val colorAlphaGray1400 = Color(0xe6010309)
+ val colorAlphaGray200 = Color(0x0f052657)
+ val colorAlphaGray300 = Color(0x1f052e61)
+ val colorAlphaGray400 = Color(0x29052551)
+ val colorAlphaGray500 = Color(0x42011d3c)
+ val colorAlphaGray600 = Color(0x59011532)
+ val colorAlphaGray700 = Color(0x7a05152e)
+ val colorAlphaGray800 = Color(0x94020e22)
+ val colorAlphaGray900 = Color(0xba030711)
+ val colorAlphaGreen100 = Color(0x0f16bb69)
+ val colorAlphaGreen1000 = Color(0xff004d36)
+ val colorAlphaGreen1100 = Color(0xff00422c)
+ val colorAlphaGreen1200 = Color(0xff003824)
+ val colorAlphaGreen1300 = Color(0xff002916)
+ val colorAlphaGreen1400 = Color(0xff002410)
+ val colorAlphaGreen200 = Color(0x1c00b85c)
+ val colorAlphaGreen300 = Color(0x3b07b661)
+ val colorAlphaGreen400 = Color(0x5205b867)
+ val colorAlphaGreen500 = Color(0x8f01b76e)
+ val colorAlphaGreen600 = Color(0xf501c18a)
+ val colorAlphaGreen700 = Color(0xff00a37d)
+ val colorAlphaGreen800 = Color(0xff00856a)
+ val colorAlphaGreen900 = Color(0xff00573e)
+ val colorAlphaLime100 = Color(0x1238d40c)
+ val colorAlphaLime1000 = Color(0xff005200)
+ val colorAlphaLime1100 = Color(0xff004200)
+ val colorAlphaLime1200 = Color(0xff003800)
+ val colorAlphaLime1300 = Color(0xff002900)
+ val colorAlphaLime1400 = Color(0xff002400)
+ val colorAlphaLime200 = Color(0x262ecf02)
+ val colorAlphaLime300 = Color(0x473ace09)
+ val colorAlphaLime400 = Color(0x6637cc05)
+ val colorAlphaLime500 = Color(0xb540ce03)
+ val colorAlphaLime600 = Color(0xdb39bd00)
+ val colorAlphaLime700 = Color(0xe6249801)
+ val colorAlphaLime800 = Color(0xf2127e02)
+ val colorAlphaLime900 = Color(0xff005700)
+ val colorAlphaOrange100 = Color(0x12ff7d1a)
+ val colorAlphaOrange1000 = Color(0xff8a0900)
+ val colorAlphaOrange1100 = Color(0xff750000)
+ val colorAlphaOrange1200 = Color(0xff660000)
+ val colorAlphaOrange1300 = Color(0xff4d0000)
+ val colorAlphaOrange1400 = Color(0xff420000)
+ val colorAlphaOrange200 = Color(0x1cff6c0a)
+ val colorAlphaOrange300 = Color(0x38ff6d05)
+ val colorAlphaOrange400 = Color(0x4dff700a)
+ val colorAlphaOrange500 = Color(0x85fc6f03)
+ val colorAlphaOrange600 = Color(0xbff56e00)
+ val colorAlphaOrange700 = Color(0xffe06c00)
+ val colorAlphaOrange800 = Color(0xffc24e00)
+ val colorAlphaOrange900 = Color(0xff941600)
+ val colorAlphaPink100 = Color(0x0aff0537)
+ val colorAlphaPink1000 = Color(0xfa830242)
+ val colorAlphaPink1100 = Color(0xff70003a)
+ val colorAlphaPink1200 = Color(0xff660030)
+ val colorAlphaPink1300 = Color(0xff4d001d)
+ val colorAlphaPink1400 = Color(0xff420015)
+ val colorAlphaPink200 = Color(0x14ff1447)
+ val colorAlphaPink300 = Color(0x21ff0037)
+ val colorAlphaPink400 = Color(0x30ff0a3f)
+ val colorAlphaPink500 = Color(0x54ff053f)
+ val colorAlphaPink600 = Color(0x78ff0040)
+ val colorAlphaPink700 = Color(0xb3f70250)
+ val colorAlphaPink800 = Color(0xf5de0265)
+ val colorAlphaPink900 = Color(0xf78f0045)
+ val colorAlphaPurple100 = Color(0x0a5338ff)
+ val colorAlphaPurple1000 = Color(0xf24600b8)
+ val colorAlphaPurple1100 = Color(0xff4300a8)
+ val colorAlphaPurple1200 = Color(0xff360094)
+ val colorAlphaPurple1300 = Color(0xff240070)
+ val colorAlphaPurple1400 = Color(0xff1f0061)
+ val colorAlphaPurple200 = Color(0x12381aff)
+ val colorAlphaPurple300 = Color(0x1f2f0fff)
+ val colorAlphaPurple400 = Color(0x292b0aff)
+ val colorAlphaPurple500 = Color(0x452b05ff)
+ val colorAlphaPurple600 = Color(0x613305ff)
+ val colorAlphaPurple700 = Color(0x873c00ff)
+ val colorAlphaPurple800 = Color(0xb34c02f7)
+ val colorAlphaPurple900 = Color(0xe64503bf)
+ val colorAlphaRed100 = Color(0x0aff391f)
+ val colorAlphaRed1000 = Color(0xff8a000b)
+ val colorAlphaRed1100 = Color(0xff750000)
+ val colorAlphaRed1200 = Color(0xff660000)
+ val colorAlphaRed1300 = Color(0xff4d0000)
+ val colorAlphaRed1400 = Color(0xff420000)
+ val colorAlphaRed200 = Color(0x14ff3814)
+ val colorAlphaRed300 = Color(0x26ff2b0a)
+ val colorAlphaRed400 = Color(0x36ff2605)
+ val colorAlphaRed500 = Color(0x5cff2205)
+ val colorAlphaRed600 = Color(0x80ff1a05)
+ val colorAlphaRed700 = Color(0xb8ff0900)
+ val colorAlphaRed800 = Color(0xe3de0211)
+ val colorAlphaRed900 = Color(0xff99001a)
+ val colorAlphaYellow100 = Color(0x21ffc70f)
+ val colorAlphaYellow1000 = Color(0xff703200)
+ val colorAlphaYellow1100 = Color(0xff612700)
+ val colorAlphaYellow1200 = Color(0xff571d00)
+ val colorAlphaYellow1300 = Color(0xff470c00)
+ val colorAlphaYellow1400 = Color(0xff3d0500)
+ val colorAlphaYellow200 = Color(0x40ffc905)
+ val colorAlphaYellow300 = Color(0x7dffc905)
+ val colorAlphaYellow400 = Color(0xb8ffcc00)
+ val colorAlphaYellow500 = Color(0xfff0bc00)
+ val colorAlphaYellow600 = Color(0xffe0a500)
+ val colorAlphaYellow700 = Color(0xffc28100)
+ val colorAlphaYellow800 = Color(0xffa86500)
+ val colorAlphaYellow900 = Color(0xff753700)
+ val colorBlue100 = Color(0xfff4f8ff)
+ val colorBlue1000 = Color(0xff053b9a)
+ val colorBlue1100 = Color(0xff043088)
+ val colorBlue1200 = Color(0xff03277b)
+ val colorBlue1300 = Color(0xff001569)
+ val colorBlue1400 = Color(0xff000c63)
+ val colorBlue200 = Color(0xffe9f2ff)
+ val colorBlue300 = Color(0xffd8e7fe)
+ val colorBlue400 = Color(0xffc8ddfd)
+ val colorBlue500 = Color(0xffa3c6fa)
+ val colorBlue600 = Color(0xff7eaff6)
+ val colorBlue700 = Color(0xff4a8ef0)
+ val colorBlue800 = Color(0xff046ee8)
+ val colorBlue900 = Color(0xff0543a7)
+ val colorCyan100 = Color(0xfff1fafb)
+ val colorCyan1000 = Color(0xff00447b)
+ val colorCyan1100 = Color(0xff00376e)
+ val colorCyan1200 = Color(0xff002e64)
+ val colorCyan1300 = Color(0xff001e53)
+ val colorCyan1400 = Color(0xff00174d)
+ val colorCyan200 = Color(0xffe3f5f8)
+ val colorCyan300 = Color(0xffc7ecf0)
+ val colorCyan400 = Color(0xffb1e4eb)
+ val colorCyan500 = Color(0xff76d1dd)
+ val colorCyan600 = Color(0xff15becf)
+ val colorCyan700 = Color(0xff009ac3)
+ val colorCyan800 = Color(0xff007ab3)
+ val colorCyan900 = Color(0xff004c84)
+ val colorFuchsia100 = Color(0xfffcf5fd)
+ val colorFuchsia1000 = Color(0xff6c1785)
+ val colorFuchsia1100 = Color(0xff5c0f76)
+ val colorFuchsia1200 = Color(0xff52026c)
+ val colorFuchsia1300 = Color(0xff3b0053)
+ val colorFuchsia1400 = Color(0xff32004a)
+ val colorFuchsia200 = Color(0xfffaeefb)
+ val colorFuchsia300 = Color(0xfff6dff7)
+ val colorFuchsia400 = Color(0xfff1d2f3)
+ val colorFuchsia500 = Color(0xffe7b2ea)
+ val colorFuchsia600 = Color(0xffdb93e1)
+ val colorFuchsia700 = Color(0xffcb68d4)
+ val colorFuchsia800 = Color(0xffb937c6)
+ val colorFuchsia900 = Color(0xff781c90)
+ val colorGray100 = Color(0xfff7f9fa)
+ val colorGray1000 = Color(0xff3f4248)
+ val colorGray1100 = Color(0xff35383d)
+ val colorGray1200 = Color(0xff2d3034)
+ val colorGray1300 = Color(0xff1f2126)
+ val colorGray1400 = Color(0xff1a1c21)
+ val colorGray200 = Color(0xfff0f2f5)
+ val colorGray300 = Color(0xffe1e6ec)
+ val colorGray400 = Color(0xffd7dce3)
+ val colorGray500 = Color(0xffbdc4cc)
+ val colorGray600 = Color(0xffa6adb7)
+ val colorGray700 = Color(0xff878f9b)
+ val colorGray800 = Color(0xff6c737e)
+ val colorGray900 = Color(0xff474a51)
+ val colorGreen100 = Color(0xfff1fbf6)
+ val colorGreen1000 = Color(0xff004d36)
+ val colorGreen1100 = Color(0xff00402b)
+ val colorGreen1200 = Color(0xff003723)
+ val colorGreen1300 = Color(0xff002715)
+ val colorGreen1400 = Color(0xff00210f)
+ val colorGreen200 = Color(0xffe3f7ed)
+ val colorGreen300 = Color(0xffc6eedb)
+ val colorGreen400 = Color(0xffafe8ce)
+ val colorGreen500 = Color(0xff71d7ae)
+ val colorGreen600 = Color(0xff0bc491)
+ val colorGreen700 = Color(0xff00a27c)
+ val colorGreen800 = Color(0xff008268)
+ val colorGreen900 = Color(0xff00553d)
+ val colorLime100 = Color(0xfff1fcee)
+ val colorLime1000 = Color(0xff004f00)
+ val colorLime1100 = Color(0xff004200)
+ val colorLime1200 = Color(0xff003900)
+ val colorLime1300 = Color(0xff002900)
+ val colorLime1400 = Color(0xff002200)
+ val colorLime200 = Color(0xffe0f8d9)
+ val colorLime300 = Color(0xffc8f1ba)
+ val colorLime400 = Color(0xffafeb9b)
+ val colorLime500 = Color(0xff76db4c)
+ val colorLime600 = Color(0xff54c424)
+ val colorLime700 = Color(0xff3aa31a)
+ val colorLime800 = Color(0xff1f850f)
+ val colorLime900 = Color(0xff005700)
+ val colorOrange100 = Color(0xfffff6ef)
+ val colorOrange1000 = Color(0xff890800)
+ val colorOrange1100 = Color(0xff770000)
+ val colorOrange1200 = Color(0xff670000)
+ val colorOrange1300 = Color(0xff4c0000)
+ val colorOrange1400 = Color(0xff420000)
+ val colorOrange200 = Color(0xffffefe4)
+ val colorOrange300 = Color(0xffffdfc8)
+ val colorOrange400 = Color(0xffffd4b5)
+ val colorOrange500 = Color(0xfffdb37c)
+ val colorOrange600 = Color(0xfff89440)
+ val colorOrange700 = Color(0xffe26e00)
+ val colorOrange800 = Color(0xffc44d00)
+ val colorOrange900 = Color(0xff931700)
+ val colorPink100 = Color(0xfffff5f7)
+ val colorPink1000 = Color(0xff840745)
+ val colorPink1100 = Color(0xff72003a)
+ val colorPink1200 = Color(0xff64002f)
+ val colorPink1300 = Color(0xff4a001c)
+ val colorPink1400 = Color(0xff410015)
+ val colorPink200 = Color(0xffffecf0)
+ val colorPink300 = Color(0xffffdee5)
+ val colorPink400 = Color(0xffffd0da)
+ val colorPink500 = Color(0xffffadc0)
+ val colorPink600 = Color(0xffff88a6)
+ val colorPink700 = Color(0xfff94e84)
+ val colorPink800 = Color(0xffe00c6a)
+ val colorPink900 = Color(0xff92084b)
+ val colorPurple100 = Color(0xfff8f7ff)
+ val colorPurple1000 = Color(0xff4f0dba)
+ val colorPurple1100 = Color(0xff4200a6)
+ val colorPurple1200 = Color(0xff360094)
+ val colorPurple1300 = Color(0xff240070)
+ val colorPurple1400 = Color(0xff1f0062)
+ val colorPurple200 = Color(0xfff1efff)
+ val colorPurple300 = Color(0xffe6e2ff)
+ val colorPurple400 = Color(0xffddd8ff)
+ val colorPurple500 = Color(0xffc5bbff)
+ val colorPurple600 = Color(0xffb1a0ff)
+ val colorPurple700 = Color(0xff9778fe)
+ val colorPurple800 = Color(0xff824ef9)
+ val colorPurple900 = Color(0xff571cc4)
+ val colorRed100 = Color(0xfffff7f6)
+ val colorRed1000 = Color(0xff8b000c)
+ val colorRed1100 = Color(0xff770000)
+ val colorRed1200 = Color(0xff670000)
+ val colorRed1300 = Color(0xff4c0000)
+ val colorRed1400 = Color(0xff420000)
+ val colorRed200 = Color(0xffffefec)
+ val colorRed300 = Color(0xffffdfda)
+ val colorRed400 = Color(0xffffd1ca)
+ val colorRed500 = Color(0xffffafa5)
+ val colorRed600 = Color(0xffff8c81)
+ val colorRed700 = Color(0xffff4e49)
+ val colorRed800 = Color(0xffe11e2a)
+ val colorRed900 = Color(0xff99001a)
+ val colorThemeBg = Color(0xffffffff)
+ val colorTransparent = Color(0x00000000)
+ val colorYellow100 = Color(0xfffff8e0)
+ val colorYellow1000 = Color(0xff6e3100)
+ val colorYellow1100 = Color(0xff612600)
+ val colorYellow1200 = Color(0xff571d00)
+ val colorYellow1300 = Color(0xff450c00)
+ val colorYellow1400 = Color(0xff3f0500)
+ val colorYellow200 = Color(0xfffff2c1)
+ val colorYellow300 = Color(0xffffe484)
+ val colorYellow400 = Color(0xffffda49)
+ val colorYellow500 = Color(0xfff1bd00)
+ val colorYellow600 = Color(0xffdea200)
+ val colorYellow700 = Color(0xffc38100)
+ val colorYellow800 = Color(0xffa76300)
+ val colorYellow900 = Color(0xff773800)
+}
diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/utils/ColorUtils.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/utils/ColorUtils.kt
new file mode 100644
index 0000000000..f0c6c12fb0
--- /dev/null
+++ b/libraries/compound/src/main/kotlin/io/element/android/compound/utils/ColorUtils.kt
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.utils
+
+import androidx.compose.ui.graphics.Color
+
+/**
+ * Convert color to Human Readable Format.
+ */
+fun Color.toHrf(): String {
+ return "0x" + value.toString(16).take(8).uppercase()
+}
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_admin.xml b/libraries/compound/src/main/res/drawable/ic_compound_admin.xml
new file mode 100644
index 0000000000..8762195e7f
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_admin.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_arrow_down.xml b/libraries/compound/src/main/res/drawable/ic_compound_arrow_down.xml
new file mode 100644
index 0000000000..6d311ac7a2
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_arrow_down.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_arrow_left.xml b/libraries/compound/src/main/res/drawable/ic_compound_arrow_left.xml
new file mode 100644
index 0000000000..4e1949895f
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_arrow_left.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_arrow_right.xml b/libraries/compound/src/main/res/drawable/ic_compound_arrow_right.xml
new file mode 100644
index 0000000000..4a42e3c16f
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_arrow_right.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_arrow_up.xml b/libraries/compound/src/main/res/drawable/ic_compound_arrow_up.xml
new file mode 100644
index 0000000000..7ff26b539a
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_arrow_up.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_arrow_up_right.xml b/libraries/compound/src/main/res/drawable/ic_compound_arrow_up_right.xml
new file mode 100644
index 0000000000..ea686b6cff
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_arrow_up_right.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_ask_to_join.xml b/libraries/compound/src/main/res/drawable/ic_compound_ask_to_join.xml
new file mode 100644
index 0000000000..2c2f779ab6
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_ask_to_join.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_ask_to_join_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_ask_to_join_solid.xml
new file mode 100644
index 0000000000..1346e2c889
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_ask_to_join_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_attachment.xml b/libraries/compound/src/main/res/drawable/ic_compound_attachment.xml
new file mode 100644
index 0000000000..4ddf7f4df7
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_attachment.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_audio.xml b/libraries/compound/src/main/res/drawable/ic_compound_audio.xml
new file mode 100644
index 0000000000..5fb9365470
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_audio.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_block.xml b/libraries/compound/src/main/res/drawable/ic_compound_block.xml
new file mode 100644
index 0000000000..08ef51dce3
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_block.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_bold.xml b/libraries/compound/src/main/res/drawable/ic_compound_bold.xml
new file mode 100644
index 0000000000..2546a230cc
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_bold.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_calendar.xml b/libraries/compound/src/main/res/drawable/ic_compound_calendar.xml
new file mode 100644
index 0000000000..72a9fe5868
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_calendar.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_chart.xml b/libraries/compound/src/main/res/drawable/ic_compound_chart.xml
new file mode 100644
index 0000000000..cc1c8fb662
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_chart.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_chat.xml b/libraries/compound/src/main/res/drawable/ic_compound_chat.xml
new file mode 100644
index 0000000000..3a7e70841b
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_chat.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_chat_new.xml b/libraries/compound/src/main/res/drawable/ic_compound_chat_new.xml
new file mode 100644
index 0000000000..8bf9f6762f
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_chat_new.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_chat_problem.xml b/libraries/compound/src/main/res/drawable/ic_compound_chat_problem.xml
new file mode 100644
index 0000000000..380358478a
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_chat_problem.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_chat_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_chat_solid.xml
new file mode 100644
index 0000000000..d08def35fe
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_chat_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_check.xml b/libraries/compound/src/main/res/drawable/ic_compound_check.xml
new file mode 100644
index 0000000000..78f935c940
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_check.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_check_circle.xml b/libraries/compound/src/main/res/drawable/ic_compound_check_circle.xml
new file mode 100644
index 0000000000..8d6147246b
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_check_circle.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_check_circle_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_check_circle_solid.xml
new file mode 100644
index 0000000000..d258a5c97a
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_check_circle_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_chevron_down.xml b/libraries/compound/src/main/res/drawable/ic_compound_chevron_down.xml
new file mode 100644
index 0000000000..2ac456e988
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_chevron_down.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_chevron_left.xml b/libraries/compound/src/main/res/drawable/ic_compound_chevron_left.xml
new file mode 100644
index 0000000000..50316a4e81
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_chevron_left.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_chevron_right.xml b/libraries/compound/src/main/res/drawable/ic_compound_chevron_right.xml
new file mode 100644
index 0000000000..d19a9daa4d
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_chevron_right.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_chevron_up.xml b/libraries/compound/src/main/res/drawable/ic_compound_chevron_up.xml
new file mode 100644
index 0000000000..d84ebade9c
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_chevron_up.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_chevron_up_down.xml b/libraries/compound/src/main/res/drawable/ic_compound_chevron_up_down.xml
new file mode 100644
index 0000000000..213dfbef9f
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_chevron_up_down.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_circle.xml b/libraries/compound/src/main/res/drawable/ic_compound_circle.xml
new file mode 100644
index 0000000000..d5a292aa3c
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_circle.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_close.xml b/libraries/compound/src/main/res/drawable/ic_compound_close.xml
new file mode 100644
index 0000000000..ef8a75f08a
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_close.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_cloud.xml b/libraries/compound/src/main/res/drawable/ic_compound_cloud.xml
new file mode 100644
index 0000000000..b96fd18250
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_cloud.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_cloud_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_cloud_solid.xml
new file mode 100644
index 0000000000..09a91809f3
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_cloud_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_code.xml b/libraries/compound/src/main/res/drawable/ic_compound_code.xml
new file mode 100644
index 0000000000..c17c73c201
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_code.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_collapse.xml b/libraries/compound/src/main/res/drawable/ic_compound_collapse.xml
new file mode 100644
index 0000000000..8cc9302197
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_collapse.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_company.xml b/libraries/compound/src/main/res/drawable/ic_compound_company.xml
new file mode 100644
index 0000000000..28a9232c13
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_company.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_compose.xml b/libraries/compound/src/main/res/drawable/ic_compound_compose.xml
new file mode 100644
index 0000000000..eaa9ca0ef1
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_compose.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_computer.xml b/libraries/compound/src/main/res/drawable/ic_compound_computer.xml
new file mode 100644
index 0000000000..97748e8ffd
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_computer.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_copy.xml b/libraries/compound/src/main/res/drawable/ic_compound_copy.xml
new file mode 100644
index 0000000000..4492c5aca9
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_copy.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_dark_mode.xml b/libraries/compound/src/main/res/drawable/ic_compound_dark_mode.xml
new file mode 100644
index 0000000000..bc4fa89cd4
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_dark_mode.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_delete.xml b/libraries/compound/src/main/res/drawable/ic_compound_delete.xml
new file mode 100644
index 0000000000..dd2e3b50d0
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_delete.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_devices.xml b/libraries/compound/src/main/res/drawable/ic_compound_devices.xml
new file mode 100644
index 0000000000..a399d675cf
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_devices.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_dial_pad.xml b/libraries/compound/src/main/res/drawable/ic_compound_dial_pad.xml
new file mode 100644
index 0000000000..1967b8df77
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_dial_pad.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_document.xml b/libraries/compound/src/main/res/drawable/ic_compound_document.xml
new file mode 100644
index 0000000000..830a36597a
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_document.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_download.xml b/libraries/compound/src/main/res/drawable/ic_compound_download.xml
new file mode 100644
index 0000000000..bf4c24ec26
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_download.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_download_ios.xml b/libraries/compound/src/main/res/drawable/ic_compound_download_ios.xml
new file mode 100644
index 0000000000..16a45ee188
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_download_ios.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_drag_grid.xml b/libraries/compound/src/main/res/drawable/ic_compound_drag_grid.xml
new file mode 100644
index 0000000000..96697b574d
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_drag_grid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_drag_list.xml b/libraries/compound/src/main/res/drawable/ic_compound_drag_list.xml
new file mode 100644
index 0000000000..57fe86c0e8
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_drag_list.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_earpiece.xml b/libraries/compound/src/main/res/drawable/ic_compound_earpiece.xml
new file mode 100644
index 0000000000..66b495b304
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_earpiece.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_edit.xml b/libraries/compound/src/main/res/drawable/ic_compound_edit.xml
new file mode 100644
index 0000000000..de0c0b345a
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_edit.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_edit_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_edit_solid.xml
new file mode 100644
index 0000000000..33da283f4d
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_edit_solid.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_email.xml b/libraries/compound/src/main/res/drawable/ic_compound_email.xml
new file mode 100644
index 0000000000..223c9620f3
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_email.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_email_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_email_solid.xml
new file mode 100644
index 0000000000..f5c98f8cfa
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_email_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_end_call.xml b/libraries/compound/src/main/res/drawable/ic_compound_end_call.xml
new file mode 100644
index 0000000000..f58a645c6e
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_end_call.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_error.xml b/libraries/compound/src/main/res/drawable/ic_compound_error.xml
new file mode 100644
index 0000000000..a4ea2f0016
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_error.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_error_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_error_solid.xml
new file mode 100644
index 0000000000..97b8ece183
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_error_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_expand.xml b/libraries/compound/src/main/res/drawable/ic_compound_expand.xml
new file mode 100644
index 0000000000..88aef8abc1
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_expand.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_explore.xml b/libraries/compound/src/main/res/drawable/ic_compound_explore.xml
new file mode 100644
index 0000000000..9b62efa291
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_explore.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_export_archive.xml b/libraries/compound/src/main/res/drawable/ic_compound_export_archive.xml
new file mode 100644
index 0000000000..f09775d0be
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_export_archive.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_extensions.xml b/libraries/compound/src/main/res/drawable/ic_compound_extensions.xml
new file mode 100644
index 0000000000..1bd8ae7bce
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_extensions.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_extensions_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_extensions_solid.xml
new file mode 100644
index 0000000000..7873a49ed6
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_extensions_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_favourite.xml b/libraries/compound/src/main/res/drawable/ic_compound_favourite.xml
new file mode 100644
index 0000000000..f7845dc080
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_favourite.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_favourite_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_favourite_solid.xml
new file mode 100644
index 0000000000..3fc393c572
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_favourite_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_file_error.xml b/libraries/compound/src/main/res/drawable/ic_compound_file_error.xml
new file mode 100644
index 0000000000..13b3571ef8
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_file_error.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_files.xml b/libraries/compound/src/main/res/drawable/ic_compound_files.xml
new file mode 100644
index 0000000000..67a30b6bd8
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_files.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_filter.xml b/libraries/compound/src/main/res/drawable/ic_compound_filter.xml
new file mode 100644
index 0000000000..2f2b0e5f10
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_filter.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_forward.xml b/libraries/compound/src/main/res/drawable/ic_compound_forward.xml
new file mode 100644
index 0000000000..e614fabb4e
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_forward.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_grid.xml b/libraries/compound/src/main/res/drawable/ic_compound_grid.xml
new file mode 100644
index 0000000000..223ed40a2f
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_grid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_group.xml b/libraries/compound/src/main/res/drawable/ic_compound_group.xml
new file mode 100644
index 0000000000..4781d1306b
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_group.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_guest.xml b/libraries/compound/src/main/res/drawable/ic_compound_guest.xml
new file mode 100644
index 0000000000..1bb2d99842
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_guest.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_headphones_off_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_headphones_off_solid.xml
new file mode 100644
index 0000000000..af81d29c1d
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_headphones_off_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_headphones_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_headphones_solid.xml
new file mode 100644
index 0000000000..eac3a666d0
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_headphones_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_help.xml b/libraries/compound/src/main/res/drawable/ic_compound_help.xml
new file mode 100644
index 0000000000..293120e8cd
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_help.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_help_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_help_solid.xml
new file mode 100644
index 0000000000..80332718c2
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_help_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_history.xml b/libraries/compound/src/main/res/drawable/ic_compound_history.xml
new file mode 100644
index 0000000000..25d4c68509
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_history.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_home.xml b/libraries/compound/src/main/res/drawable/ic_compound_home.xml
new file mode 100644
index 0000000000..0feb167199
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_home.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_home_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_home_solid.xml
new file mode 100644
index 0000000000..10cca67a37
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_home_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_host.xml b/libraries/compound/src/main/res/drawable/ic_compound_host.xml
new file mode 100644
index 0000000000..d45d1c0278
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_host.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_image.xml b/libraries/compound/src/main/res/drawable/ic_compound_image.xml
new file mode 100644
index 0000000000..58a3525b3a
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_image.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_image_error.xml b/libraries/compound/src/main/res/drawable/ic_compound_image_error.xml
new file mode 100644
index 0000000000..1c8d716166
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_image_error.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_indent_decrease.xml b/libraries/compound/src/main/res/drawable/ic_compound_indent_decrease.xml
new file mode 100644
index 0000000000..5ada985743
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_indent_decrease.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_indent_increase.xml b/libraries/compound/src/main/res/drawable/ic_compound_indent_increase.xml
new file mode 100644
index 0000000000..de5df3977a
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_indent_increase.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_info.xml b/libraries/compound/src/main/res/drawable/ic_compound_info.xml
new file mode 100644
index 0000000000..cf0318bb61
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_info.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_info_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_info_solid.xml
new file mode 100644
index 0000000000..9101aacffc
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_info_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_inline_code.xml b/libraries/compound/src/main/res/drawable/ic_compound_inline_code.xml
new file mode 100644
index 0000000000..9f78e1f8c2
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_inline_code.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_italic.xml b/libraries/compound/src/main/res/drawable/ic_compound_italic.xml
new file mode 100644
index 0000000000..e3fee1f82e
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_italic.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_key.xml b/libraries/compound/src/main/res/drawable/ic_compound_key.xml
new file mode 100644
index 0000000000..112c16d78f
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_key.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_key_off.xml b/libraries/compound/src/main/res/drawable/ic_compound_key_off.xml
new file mode 100644
index 0000000000..b160e81c73
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_key_off.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_key_off_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_key_off_solid.xml
new file mode 100644
index 0000000000..c1961ec390
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_key_off_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_key_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_key_solid.xml
new file mode 100644
index 0000000000..29f9ad1a42
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_key_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_keyboard.xml b/libraries/compound/src/main/res/drawable/ic_compound_keyboard.xml
new file mode 100644
index 0000000000..6216c1ccf8
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_keyboard.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_labs.xml b/libraries/compound/src/main/res/drawable/ic_compound_labs.xml
new file mode 100644
index 0000000000..d0bcae3bab
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_labs.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_leave.xml b/libraries/compound/src/main/res/drawable/ic_compound_leave.xml
new file mode 100644
index 0000000000..ad5897d4f2
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_leave.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_link.xml b/libraries/compound/src/main/res/drawable/ic_compound_link.xml
new file mode 100644
index 0000000000..3787cf3981
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_link.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_linux.xml b/libraries/compound/src/main/res/drawable/ic_compound_linux.xml
new file mode 100644
index 0000000000..bb3216e729
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_linux.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_list_bulleted.xml b/libraries/compound/src/main/res/drawable/ic_compound_list_bulleted.xml
new file mode 100644
index 0000000000..f7fe7a8256
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_list_bulleted.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_list_numbered.xml b/libraries/compound/src/main/res/drawable/ic_compound_list_numbered.xml
new file mode 100644
index 0000000000..d3fbeb2d2d
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_list_numbered.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_list_view.xml b/libraries/compound/src/main/res/drawable/ic_compound_list_view.xml
new file mode 100644
index 0000000000..ffd3359cfa
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_list_view.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_location_navigator.xml b/libraries/compound/src/main/res/drawable/ic_compound_location_navigator.xml
new file mode 100644
index 0000000000..bffa2d9f9d
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_location_navigator.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_location_navigator_centred.xml b/libraries/compound/src/main/res/drawable/ic_compound_location_navigator_centred.xml
new file mode 100644
index 0000000000..50f8ac655d
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_location_navigator_centred.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_location_pin.xml b/libraries/compound/src/main/res/drawable/ic_compound_location_pin.xml
new file mode 100644
index 0000000000..063076d4ae
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_location_pin.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_location_pin_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_location_pin_solid.xml
new file mode 100644
index 0000000000..defb2d4e42
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_location_pin_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_lock.xml b/libraries/compound/src/main/res/drawable/ic_compound_lock.xml
new file mode 100644
index 0000000000..9715295d94
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_lock.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_lock_off.xml b/libraries/compound/src/main/res/drawable/ic_compound_lock_off.xml
new file mode 100644
index 0000000000..a19bac4811
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_lock_off.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_lock_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_lock_solid.xml
new file mode 100644
index 0000000000..6a5a11b4de
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_lock_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_mac.xml b/libraries/compound/src/main/res/drawable/ic_compound_mac.xml
new file mode 100644
index 0000000000..6f72038f43
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_mac.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_mark_as_read.xml b/libraries/compound/src/main/res/drawable/ic_compound_mark_as_read.xml
new file mode 100644
index 0000000000..8de67ac4e8
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_mark_as_read.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_mark_as_unread.xml b/libraries/compound/src/main/res/drawable/ic_compound_mark_as_unread.xml
new file mode 100644
index 0000000000..6b3c53144a
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_mark_as_unread.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_mark_threads_as_read.xml b/libraries/compound/src/main/res/drawable/ic_compound_mark_threads_as_read.xml
new file mode 100644
index 0000000000..5d6c12a557
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_mark_threads_as_read.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_marker_read_receipts.xml b/libraries/compound/src/main/res/drawable/ic_compound_marker_read_receipts.xml
new file mode 100644
index 0000000000..c70cfd3e51
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_marker_read_receipts.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_mention.xml b/libraries/compound/src/main/res/drawable/ic_compound_mention.xml
new file mode 100644
index 0000000000..274ccb3775
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_mention.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_menu.xml b/libraries/compound/src/main/res/drawable/ic_compound_menu.xml
new file mode 100644
index 0000000000..c3ee1a2ee1
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_menu.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_mic_off.xml b/libraries/compound/src/main/res/drawable/ic_compound_mic_off.xml
new file mode 100644
index 0000000000..e28210c07d
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_mic_off.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_mic_off_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_mic_off_solid.xml
new file mode 100644
index 0000000000..686809342a
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_mic_off_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_mic_on.xml b/libraries/compound/src/main/res/drawable/ic_compound_mic_on.xml
new file mode 100644
index 0000000000..793b9c8f83
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_mic_on.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_mic_on_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_mic_on_solid.xml
new file mode 100644
index 0000000000..026f477e75
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_mic_on_solid.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_minus.xml b/libraries/compound/src/main/res/drawable/ic_compound_minus.xml
new file mode 100644
index 0000000000..064946b6dc
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_minus.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_mobile.xml b/libraries/compound/src/main/res/drawable/ic_compound_mobile.xml
new file mode 100644
index 0000000000..8257628868
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_mobile.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_notifications.xml b/libraries/compound/src/main/res/drawable/ic_compound_notifications.xml
new file mode 100644
index 0000000000..afd16aa8e9
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_notifications.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_notifications_off.xml b/libraries/compound/src/main/res/drawable/ic_compound_notifications_off.xml
new file mode 100644
index 0000000000..e4fca897a3
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_notifications_off.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_notifications_off_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_notifications_off_solid.xml
new file mode 100644
index 0000000000..4bc9fdd2ab
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_notifications_off_solid.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_notifications_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_notifications_solid.xml
new file mode 100644
index 0000000000..358a18c83e
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_notifications_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_offline.xml b/libraries/compound/src/main/res/drawable/ic_compound_offline.xml
new file mode 100644
index 0000000000..322954e4b0
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_offline.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_overflow_horizontal.xml b/libraries/compound/src/main/res/drawable/ic_compound_overflow_horizontal.xml
new file mode 100644
index 0000000000..c85d1e8da2
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_overflow_horizontal.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_overflow_vertical.xml b/libraries/compound/src/main/res/drawable/ic_compound_overflow_vertical.xml
new file mode 100644
index 0000000000..269e28613c
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_overflow_vertical.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_pause.xml b/libraries/compound/src/main/res/drawable/ic_compound_pause.xml
new file mode 100644
index 0000000000..bcd8325107
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_pause.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_pause_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_pause_solid.xml
new file mode 100644
index 0000000000..f25a7cbcfc
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_pause_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_pin.xml b/libraries/compound/src/main/res/drawable/ic_compound_pin.xml
new file mode 100644
index 0000000000..0aa36f53e9
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_pin.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_pin_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_pin_solid.xml
new file mode 100644
index 0000000000..9326b0fd75
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_pin_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_play.xml b/libraries/compound/src/main/res/drawable/ic_compound_play.xml
new file mode 100644
index 0000000000..d7b8d3c5e5
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_play.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_play_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_play_solid.xml
new file mode 100644
index 0000000000..fca650ccd8
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_play_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_plus.xml b/libraries/compound/src/main/res/drawable/ic_compound_plus.xml
new file mode 100644
index 0000000000..a20a59aac9
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_plus.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_polls.xml b/libraries/compound/src/main/res/drawable/ic_compound_polls.xml
new file mode 100644
index 0000000000..8c0e2b5a45
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_polls.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_polls_end.xml b/libraries/compound/src/main/res/drawable/ic_compound_polls_end.xml
new file mode 100644
index 0000000000..8cfe2be80e
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_polls_end.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_pop_out.xml b/libraries/compound/src/main/res/drawable/ic_compound_pop_out.xml
new file mode 100644
index 0000000000..7b5b07b969
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_pop_out.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_preferences.xml b/libraries/compound/src/main/res/drawable/ic_compound_preferences.xml
new file mode 100644
index 0000000000..fbc271730d
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_preferences.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_presence_outline_8_x_8.xml b/libraries/compound/src/main/res/drawable/ic_compound_presence_outline_8_x_8.xml
new file mode 100644
index 0000000000..3d815c0696
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_presence_outline_8_x_8.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_presence_solid_8_x_8.xml b/libraries/compound/src/main/res/drawable/ic_compound_presence_solid_8_x_8.xml
new file mode 100644
index 0000000000..ab82df2441
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_presence_solid_8_x_8.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_presence_strikethrough_8_x_8.xml b/libraries/compound/src/main/res/drawable/ic_compound_presence_strikethrough_8_x_8.xml
new file mode 100644
index 0000000000..1ed8a1e492
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_presence_strikethrough_8_x_8.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_public.xml b/libraries/compound/src/main/res/drawable/ic_compound_public.xml
new file mode 100644
index 0000000000..3dfd7046ea
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_public.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_qr_code.xml b/libraries/compound/src/main/res/drawable/ic_compound_qr_code.xml
new file mode 100644
index 0000000000..4befbcef95
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_qr_code.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_quote.xml b/libraries/compound/src/main/res/drawable/ic_compound_quote.xml
new file mode 100644
index 0000000000..728fe07e43
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_quote.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_raised_hand_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_raised_hand_solid.xml
new file mode 100644
index 0000000000..d345569ead
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_raised_hand_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_reaction.xml b/libraries/compound/src/main/res/drawable/ic_compound_reaction.xml
new file mode 100644
index 0000000000..f85f57d002
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_reaction.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_reaction_add.xml b/libraries/compound/src/main/res/drawable/ic_compound_reaction_add.xml
new file mode 100644
index 0000000000..3fe88d4f8a
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_reaction_add.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_reaction_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_reaction_solid.xml
new file mode 100644
index 0000000000..826ac69830
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_reaction_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_reply.xml b/libraries/compound/src/main/res/drawable/ic_compound_reply.xml
new file mode 100644
index 0000000000..45a797deda
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_reply.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_restart.xml b/libraries/compound/src/main/res/drawable/ic_compound_restart.xml
new file mode 100644
index 0000000000..9360979845
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_restart.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_room.xml b/libraries/compound/src/main/res/drawable/ic_compound_room.xml
new file mode 100644
index 0000000000..a0a278eab7
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_room.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_search.xml b/libraries/compound/src/main/res/drawable/ic_compound_search.xml
new file mode 100644
index 0000000000..8e1ec94374
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_search.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_send.xml b/libraries/compound/src/main/res/drawable/ic_compound_send.xml
new file mode 100644
index 0000000000..8c0d5e1159
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_send.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_send_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_send_solid.xml
new file mode 100644
index 0000000000..3ac0bc6f62
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_send_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_settings.xml b/libraries/compound/src/main/res/drawable/ic_compound_settings.xml
new file mode 100644
index 0000000000..7a79aa33ec
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_settings.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_settings_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_settings_solid.xml
new file mode 100644
index 0000000000..7a75b037ba
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_settings_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_share.xml b/libraries/compound/src/main/res/drawable/ic_compound_share.xml
new file mode 100644
index 0000000000..6abf169f20
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_share.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_share_android.xml b/libraries/compound/src/main/res/drawable/ic_compound_share_android.xml
new file mode 100644
index 0000000000..a92b79a76a
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_share_android.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_share_ios.xml b/libraries/compound/src/main/res/drawable/ic_compound_share_ios.xml
new file mode 100644
index 0000000000..e481978c28
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_share_ios.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_share_screen.xml b/libraries/compound/src/main/res/drawable/ic_compound_share_screen.xml
new file mode 100644
index 0000000000..89f63b897c
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_share_screen.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_share_screen_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_share_screen_solid.xml
new file mode 100644
index 0000000000..ee9a1aa837
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_share_screen_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_shield.xml b/libraries/compound/src/main/res/drawable/ic_compound_shield.xml
new file mode 100644
index 0000000000..7d2ee79157
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_shield.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_sidebar.xml b/libraries/compound/src/main/res/drawable/ic_compound_sidebar.xml
new file mode 100644
index 0000000000..4bd5a0b3e3
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_sidebar.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_sign_out.xml b/libraries/compound/src/main/res/drawable/ic_compound_sign_out.xml
new file mode 100644
index 0000000000..a6ded2b7a8
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_sign_out.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_spinner.xml b/libraries/compound/src/main/res/drawable/ic_compound_spinner.xml
new file mode 100644
index 0000000000..80721fdce8
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_spinner.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_spotlight.xml b/libraries/compound/src/main/res/drawable/ic_compound_spotlight.xml
new file mode 100644
index 0000000000..acaad53b3b
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_spotlight.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_spotlight_view.xml b/libraries/compound/src/main/res/drawable/ic_compound_spotlight_view.xml
new file mode 100644
index 0000000000..724cdf91cf
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_spotlight_view.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_strikethrough.xml b/libraries/compound/src/main/res/drawable/ic_compound_strikethrough.xml
new file mode 100644
index 0000000000..a472c6c97e
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_strikethrough.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_switch_camera_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_switch_camera_solid.xml
new file mode 100644
index 0000000000..a7695e40ff
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_switch_camera_solid.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_take_photo.xml b/libraries/compound/src/main/res/drawable/ic_compound_take_photo.xml
new file mode 100644
index 0000000000..d6dbb4d568
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_take_photo.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_take_photo_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_take_photo_solid.xml
new file mode 100644
index 0000000000..2db8a6cdf0
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_take_photo_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_text_formatting.xml b/libraries/compound/src/main/res/drawable/ic_compound_text_formatting.xml
new file mode 100644
index 0000000000..4126080fa9
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_text_formatting.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_threads.xml b/libraries/compound/src/main/res/drawable/ic_compound_threads.xml
new file mode 100644
index 0000000000..4fa1e877d1
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_threads.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_threads_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_threads_solid.xml
new file mode 100644
index 0000000000..4db292231d
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_threads_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_time.xml b/libraries/compound/src/main/res/drawable/ic_compound_time.xml
new file mode 100644
index 0000000000..b29bb29de7
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_time.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_underline.xml b/libraries/compound/src/main/res/drawable/ic_compound_underline.xml
new file mode 100644
index 0000000000..c90d6bc591
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_underline.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_unknown.xml b/libraries/compound/src/main/res/drawable/ic_compound_unknown.xml
new file mode 100644
index 0000000000..87afe69e02
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_unknown.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_unknown_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_unknown_solid.xml
new file mode 100644
index 0000000000..89b6a89056
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_unknown_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_unpin.xml b/libraries/compound/src/main/res/drawable/ic_compound_unpin.xml
new file mode 100644
index 0000000000..c7aee251f4
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_unpin.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_user.xml b/libraries/compound/src/main/res/drawable/ic_compound_user.xml
new file mode 100644
index 0000000000..948bda3e4e
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_user.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_user_add.xml b/libraries/compound/src/main/res/drawable/ic_compound_user_add.xml
new file mode 100644
index 0000000000..81d9aaf1bb
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_user_add.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_user_add_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_user_add_solid.xml
new file mode 100644
index 0000000000..693db72c06
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_user_add_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_user_profile.xml b/libraries/compound/src/main/res/drawable/ic_compound_user_profile.xml
new file mode 100644
index 0000000000..222b9510ea
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_user_profile.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_user_profile_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_user_profile_solid.xml
new file mode 100644
index 0000000000..5d268642e0
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_user_profile_solid.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_user_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_user_solid.xml
new file mode 100644
index 0000000000..c281d6f009
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_user_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_verified.xml b/libraries/compound/src/main/res/drawable/ic_compound_verified.xml
new file mode 100644
index 0000000000..f30bec93ff
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_verified.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_video_call.xml b/libraries/compound/src/main/res/drawable/ic_compound_video_call.xml
new file mode 100644
index 0000000000..4f1c9b1ba7
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_video_call.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_video_call_declined_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_video_call_declined_solid.xml
new file mode 100644
index 0000000000..a6b699c6bd
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_video_call_declined_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_video_call_missed_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_video_call_missed_solid.xml
new file mode 100644
index 0000000000..57c1502560
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_video_call_missed_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_video_call_off.xml b/libraries/compound/src/main/res/drawable/ic_compound_video_call_off.xml
new file mode 100644
index 0000000000..6466ec2848
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_video_call_off.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_video_call_off_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_video_call_off_solid.xml
new file mode 100644
index 0000000000..35a5796577
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_video_call_off_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_video_call_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_video_call_solid.xml
new file mode 100644
index 0000000000..ec4cf0902e
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_video_call_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_visibility_off.xml b/libraries/compound/src/main/res/drawable/ic_compound_visibility_off.xml
new file mode 100644
index 0000000000..c4234584a3
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_visibility_off.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_visibility_on.xml b/libraries/compound/src/main/res/drawable/ic_compound_visibility_on.xml
new file mode 100644
index 0000000000..a66dcfa429
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_visibility_on.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_voice_call.xml b/libraries/compound/src/main/res/drawable/ic_compound_voice_call.xml
new file mode 100644
index 0000000000..579738d57b
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_voice_call.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_voice_call_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_voice_call_solid.xml
new file mode 100644
index 0000000000..428e2fa3d1
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_voice_call_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_volume_off.xml b/libraries/compound/src/main/res/drawable/ic_compound_volume_off.xml
new file mode 100644
index 0000000000..e240c01b0b
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_volume_off.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_volume_off_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_volume_off_solid.xml
new file mode 100644
index 0000000000..6c801e7a16
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_volume_off_solid.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_volume_on.xml b/libraries/compound/src/main/res/drawable/ic_compound_volume_on.xml
new file mode 100644
index 0000000000..453a2578e2
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_volume_on.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_volume_on_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_volume_on_solid.xml
new file mode 100644
index 0000000000..232571342f
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_volume_on_solid.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_warning.xml b/libraries/compound/src/main/res/drawable/ic_compound_warning.xml
new file mode 100644
index 0000000000..53b838ca56
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_warning.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_web_browser.xml b/libraries/compound/src/main/res/drawable/ic_compound_web_browser.xml
new file mode 100644
index 0000000000..baaf15c059
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_web_browser.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_windows.xml b/libraries/compound/src/main/res/drawable/ic_compound_windows.xml
new file mode 100644
index 0000000000..37a3915dc8
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_windows.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_workspace.xml b/libraries/compound/src/main/res/drawable/ic_compound_workspace.xml
new file mode 100644
index 0000000000..3871fde4b4
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_workspace.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/main/res/drawable/ic_compound_workspace_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_workspace_solid.xml
new file mode 100644
index 0000000000..51c5c9f9cb
--- /dev/null
+++ b/libraries/compound/src/main/res/drawable/ic_compound_workspace_solid.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/AvatarColorsTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/AvatarColorsTest.kt
new file mode 100644
index 0000000000..015cd5341c
--- /dev/null
+++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/AvatarColorsTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.screenshot
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.github.takahirom.roborazzi.captureRoboImage
+import io.element.android.compound.screenshot.utils.screenshotFile
+import io.element.android.compound.theme.AvatarColorsPreviewDark
+import io.element.android.compound.theme.AvatarColorsPreviewLight
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.GraphicsMode
+
+@RunWith(AndroidJUnit4::class)
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+class AvatarColorsTest {
+ @Test
+ @Config(sdk = [35], qualifiers = "xxhdpi")
+ fun screenshots() {
+ captureRoboImage(file = screenshotFile("Avatar Colors - Light.png")) {
+ AvatarColorsPreviewLight()
+ }
+ captureRoboImage(file = screenshotFile("Avatar Colors - Dark.png")) {
+ AvatarColorsPreviewDark()
+ }
+ }
+}
diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/CompoundIconTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/CompoundIconTest.kt
new file mode 100644
index 0000000000..9ae73a6205
--- /dev/null
+++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/CompoundIconTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.screenshot
+
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.github.takahirom.roborazzi.captureRoboImage
+import io.element.android.compound.previews.IconsCompoundPreviewDark
+import io.element.android.compound.previews.IconsCompoundPreviewLight
+import io.element.android.compound.previews.IconsCompoundPreviewRtl
+import io.element.android.compound.previews.IconsPreview
+import io.element.android.compound.screenshot.utils.screenshotFile
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import kotlinx.collections.immutable.toImmutableList
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.GraphicsMode
+
+@RunWith(AndroidJUnit4::class)
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+class CompoundIconTest {
+ @Test
+ @Config(sdk = [35], qualifiers = "w1024dp-h2048dp")
+ fun screenshots() {
+ captureRoboImage(file = screenshotFile("Compound Icons - Light.png")) {
+ IconsCompoundPreviewLight()
+ }
+ captureRoboImage(file = screenshotFile("Compound Icons - Rtl.png")) {
+ IconsCompoundPreviewRtl()
+ }
+ captureRoboImage(file = screenshotFile("Compound Icons - Dark.png")) {
+ IconsCompoundPreviewDark()
+ }
+ captureRoboImage(file = screenshotFile("Compound Vector Icons - Light.png")) {
+ val content: List<@Composable ColumnScope.() -> Unit> = CompoundIcons.all.map {
+ @Composable { Icon(imageVector = it, contentDescription = null) }
+ }
+ ElementTheme {
+ IconsPreview(
+ title = "Compound Vector Icons",
+ content = content.toImmutableList()
+ )
+ }
+ }
+ captureRoboImage(file = screenshotFile("Compound Vector Icons - Dark.png")) {
+ val content: List<@Composable ColumnScope.() -> Unit> = CompoundIcons.all.map {
+ @Composable { Icon(imageVector = it, contentDescription = null) }
+ }
+ ElementTheme(darkTheme = true) {
+ IconsPreview(
+ title = "Compound Vector Icons",
+ content = content.toImmutableList()
+ )
+ }
+ }
+ }
+}
diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/CompoundTypographyTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/CompoundTypographyTest.kt
new file mode 100644
index 0000000000..2d50e6287d
--- /dev/null
+++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/CompoundTypographyTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.screenshot
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.github.takahirom.roborazzi.captureRoboImage
+import io.element.android.compound.screenshot.utils.screenshotFile
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.TypographyTokens
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.GraphicsMode
+
+@RunWith(AndroidJUnit4::class)
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+class CompoundTypographyTest {
+ @Test
+ @Config(sdk = [35], qualifiers = "h2048dp-xxhdpi")
+ fun screenshots() {
+ captureRoboImage(file = screenshotFile("Compound Typography.png")) {
+ ElementTheme {
+ Surface {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ with(TypographyTokens) {
+ TypographyTokenPreview(fontHeadingXlBold, "Heading XL Bold")
+ TypographyTokenPreview(fontHeadingXlRegular, "Heading XL Regular")
+ TypographyTokenPreview(fontHeadingLgBold, "Heading LG Bold")
+ TypographyTokenPreview(fontHeadingLgRegular, "Heading LG Regular")
+ TypographyTokenPreview(fontHeadingMdBold, "Heading MD Bold")
+ TypographyTokenPreview(fontHeadingMdRegular, "Heading MD Regular")
+ TypographyTokenPreview(fontHeadingSmMedium, "Heading SM Medium")
+ TypographyTokenPreview(fontHeadingSmRegular, "Heading SM Regular")
+ TypographyTokenPreview(fontBodyLgMedium, "Body LG Medium")
+ TypographyTokenPreview(fontBodyLgRegular, "Body LG Regular")
+ TypographyTokenPreview(fontBodyMdMedium, "Body MD Medium")
+ TypographyTokenPreview(fontBodyMdRegular, "Body MD Regular")
+ TypographyTokenPreview(fontBodySmMedium, "Body SM Medium")
+ TypographyTokenPreview(fontBodySmRegular, "Body SM Regular")
+ TypographyTokenPreview(fontBodyXsMedium, "Body XS Medium")
+ TypographyTokenPreview(fontBodyXsRegular, "Body XS Regular")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun TypographyTokenPreview(style: TextStyle, text: String) {
+ Text(text = text, style = style)
+ }
+}
diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/ForcedDarkElementThemeTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/ForcedDarkElementThemeTest.kt
new file mode 100644
index 0000000000..341b7cb650
--- /dev/null
+++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/ForcedDarkElementThemeTest.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2023, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.screenshot
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.github.takahirom.roborazzi.captureRoboImage
+import io.element.android.compound.screenshot.utils.screenshotFile
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.theme.ForcedDarkElementTheme
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.GraphicsMode
+
+@RunWith(AndroidJUnit4::class)
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+class ForcedDarkElementThemeTest {
+ @Test
+ @Config(sdk = [35], qualifiers = "xxhdpi")
+ fun screenshots() {
+ captureRoboImage(file = screenshotFile("ForcedDarkElementTheme.png")) {
+ ElementTheme {
+ Surface {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Text(text = "Outside")
+ ForcedDarkElementTheme {
+ Surface {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(text = "Inside ForcedDarkElementTheme", modifier = Modifier.align(Alignment.Center))
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/LegacyColorsTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/LegacyColorsTest.kt
new file mode 100644
index 0000000000..ecfbbe81cd
--- /dev/null
+++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/LegacyColorsTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2023, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.screenshot
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.github.takahirom.roborazzi.captureRoboImage
+import io.element.android.compound.previews.ColorPreview
+import io.element.android.compound.screenshot.utils.screenshotFile
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.theme.LinkColor
+import io.element.android.compound.theme.SnackBarLabelColorDark
+import io.element.android.compound.theme.SnackBarLabelColorLight
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.GraphicsMode
+
+@RunWith(AndroidJUnit4::class)
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+class LegacyColorsTest {
+ @Test
+ @Config(sdk = [35], qualifiers = "xxhdpi")
+ fun screenshots() {
+ captureRoboImage(file = screenshotFile("Legacy Colors.png")) {
+ ElementTheme {
+ Surface {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(text = "Legacy Colors")
+ Spacer(modifier = Modifier.height(10.dp))
+ LegacyColorPreview(
+ color = LinkColor,
+ name = "Link"
+ )
+ LegacyColorPreview(
+ color = SnackBarLabelColorLight,
+ name = "SnackBar Label - Light"
+ )
+ LegacyColorPreview(
+ color = SnackBarLabelColorDark,
+ name = "SnackBar Label - Dark"
+ )
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun LegacyColorPreview(color: Color, name: String) {
+ ColorPreview(
+ backgroundColor = Color.White,
+ foregroundColor = Color.Black,
+ name = name,
+ color = color
+ )
+ }
+}
diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialColorSchemeTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialColorSchemeTest.kt
new file mode 100644
index 0000000000..7542eaa4fc
--- /dev/null
+++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialColorSchemeTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2023, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.screenshot
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.github.takahirom.roborazzi.captureRoboImage
+import io.element.android.compound.screenshot.utils.screenshotFile
+import io.element.android.compound.theme.ColorsSchemeDarkHcPreview
+import io.element.android.compound.theme.ColorsSchemeDarkPreview
+import io.element.android.compound.theme.ColorsSchemeLightHcPreview
+import io.element.android.compound.theme.ColorsSchemeLightPreview
+import io.element.android.compound.theme.ElementTheme
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.GraphicsMode
+
+@RunWith(AndroidJUnit4::class)
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+class MaterialColorSchemeTest {
+ @Test
+ @Config(sdk = [35], qualifiers = "h2048dp-xhdpi")
+ fun screenshots() {
+ captureRoboImage(file = screenshotFile("Material3 Colors - Light.png")) {
+ ElementTheme {
+ Surface {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ text = "M3 Light colors",
+ style = TextStyle.Default.copy(fontSize = 18.sp),
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ ColorsSchemeLightPreview()
+ }
+ }
+ }
+ }
+ captureRoboImage(file = screenshotFile("Material3 Colors - Light HC.png")) {
+ ElementTheme {
+ Surface {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ text = "M3 Light HC colors",
+ style = TextStyle.Default.copy(fontSize = 18.sp),
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ ColorsSchemeLightHcPreview()
+ }
+ }
+ }
+ }
+ captureRoboImage(file = screenshotFile("Material3 Colors - Dark.png")) {
+ ElementTheme {
+ Surface {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ text = "M3 Dark colors",
+ style = TextStyle.Default.copy(fontSize = 18.sp),
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ ColorsSchemeDarkPreview()
+ }
+ }
+ }
+ }
+ captureRoboImage(file = screenshotFile("Material3 Colors - Dark HC.png")) {
+ ElementTheme {
+ Surface {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ text = "M3 Dark HC colors",
+ style = TextStyle.Default.copy(fontSize = 18.sp),
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ ColorsSchemeDarkHcPreview()
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialTextTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialTextTest.kt
new file mode 100644
index 0000000000..764d4de77c
--- /dev/null
+++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialTextTest.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.screenshot
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.github.takahirom.roborazzi.captureRoboImage
+import io.element.android.compound.screenshot.utils.screenshotFile
+import io.element.android.compound.theme.MaterialTextPreview
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.GraphicsMode
+
+@RunWith(AndroidJUnit4::class)
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+class MaterialTextTest {
+ @Test
+ @Config(sdk = [35], qualifiers = "w480dp-h1200dp-xxhdpi")
+ fun screenshots() {
+ captureRoboImage(file = screenshotFile("MaterialText Colors.png")) {
+ MaterialTextPreview()
+ }
+ }
+}
diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialTypographyTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialTypographyTest.kt
new file mode 100644
index 0000000000..4f77a070ed
--- /dev/null
+++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialTypographyTest.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.screenshot
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.github.takahirom.roborazzi.captureRoboImage
+import io.element.android.compound.previews.TypographyPreview
+import io.element.android.compound.screenshot.utils.screenshotFile
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.GraphicsMode
+
+@RunWith(AndroidJUnit4::class)
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+class MaterialTypographyTest {
+ @Test
+ @Config(sdk = [35], qualifiers = "h2048dp-xxhdpi")
+ fun screenshots() {
+ captureRoboImage(file = screenshotFile("Material Typography.png")) {
+ TypographyPreview()
+ }
+ }
+}
diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialYouThemeTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialYouThemeTest.kt
new file mode 100644
index 0000000000..694d2c9fc6
--- /dev/null
+++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialYouThemeTest.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.screenshot
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.github.takahirom.roborazzi.captureRoboImage
+import io.element.android.compound.previews.ColorsSchemePreview
+import io.element.android.compound.screenshot.utils.screenshotFile
+import io.element.android.compound.theme.ElementTheme
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.GraphicsMode
+
+@RunWith(AndroidJUnit4::class)
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+class MaterialYouThemeTest {
+ @Test
+ @Config(sdk = [35], qualifiers = "h2048dp-xhdpi")
+ fun screenshots() {
+ captureRoboImage(file = screenshotFile("MaterialYou Theme - Light.png")) {
+ ElementTheme(dynamicColor = true) {
+ Surface {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Text(text = "Material You Theme - Light")
+ Spacer(modifier = Modifier.height(12.dp))
+ ColorsSchemePreview(Color.White, Color.Black, ElementTheme.materialColors)
+ }
+ }
+ }
+ }
+ captureRoboImage(file = screenshotFile("MaterialYou Theme - Dark.png")) {
+ ElementTheme(dynamicColor = true, darkTheme = true) {
+ Surface {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Text(text = "Material You Theme - Dark")
+ Spacer(modifier = Modifier.height(12.dp))
+ ColorsSchemePreview(Color.White, Color.Black, ElementTheme.materialColors)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/SemanticColorsTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/SemanticColorsTest.kt
new file mode 100644
index 0000000000..7e5fabd96a
--- /dev/null
+++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/SemanticColorsTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.screenshot
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.github.takahirom.roborazzi.captureRoboImage
+import io.element.android.compound.previews.CompoundSemanticColorsDark
+import io.element.android.compound.previews.CompoundSemanticColorsDarkHc
+import io.element.android.compound.previews.CompoundSemanticColorsLight
+import io.element.android.compound.previews.CompoundSemanticColorsLightHc
+import io.element.android.compound.screenshot.utils.screenshotFile
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.GraphicsMode
+
+@RunWith(AndroidJUnit4::class)
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+class SemanticColorsTest {
+ @Config(sdk = [35], qualifiers = "h2000dp-xhdpi")
+ @Test
+ fun screenshots() {
+ captureRoboImage(file = screenshotFile("Compound Semantic Colors - Light.png")) {
+ CompoundSemanticColorsLight()
+ }
+
+ captureRoboImage(file = screenshotFile("Compound Semantic Colors - Light HC.png")) {
+ CompoundSemanticColorsLightHc()
+ }
+
+ captureRoboImage(file = screenshotFile("Compound Semantic Colors - Dark.png")) {
+ CompoundSemanticColorsDark()
+ }
+
+ captureRoboImage(file = screenshotFile("Compound Semantic Colors - Dark HC.png")) {
+ CompoundSemanticColorsDarkHc()
+ }
+ }
+}
diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/utils/ScreenshotUtils.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/utils/ScreenshotUtils.kt
new file mode 100644
index 0000000000..3d4c9b3824
--- /dev/null
+++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/utils/ScreenshotUtils.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2023, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.screenshot.utils
+
+import java.io.File
+
+/**
+ * Returns a [File] object for a screenshot with the given [filename].
+ * This is to ensure we have a consistent location for all screenshots.
+ */
+fun screenshotFile(filename: String): File {
+ return File("screenshots", filename)
+}
diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt
new file mode 100644
index 0000000000..17cb2425dc
--- /dev/null
+++ b/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.compound.theme
+
+import android.content.res.Configuration
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalConfiguration
+import app.cash.molecule.RecompositionMode
+import app.cash.molecule.moleculeFlow
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class ThemeTest {
+ @Test
+ fun `isDark for System dark returns true`() {
+ `isDark for System`(
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ expected = true,
+ )
+ }
+
+ @Test
+ fun `isDark for System light return false`() {
+ `isDark for System`(
+ uiMode = Configuration.UI_MODE_NIGHT_NO,
+ expected = false,
+ )
+ }
+
+ fun `isDark for System`(
+ uiMode: Int,
+ expected: Boolean,
+ ) = runTest {
+ moleculeFlow(RecompositionMode.Immediate) {
+ var result: Boolean? = null
+ CompositionLocalProvider(
+ // Let set the system to dark
+ LocalConfiguration provides Configuration().apply {
+ this.uiMode = uiMode
+ },
+ ) {
+ result = Theme.System.isDark()
+ }
+ result
+ }.test {
+ assertThat(awaitItem()).isEqualTo(expected)
+ }
+ }
+
+ @Test
+ fun `isDark for Light returns false`() = runTest {
+ moleculeFlow(RecompositionMode.Immediate) {
+ Theme.Light.isDark()
+ }.test {
+ assertThat(awaitItem()).isFalse()
+ }
+ }
+
+ @Test
+ fun `isDark for Dark returns true`() = runTest {
+ moleculeFlow(RecompositionMode.Immediate) {
+ Theme.Dark.isDark()
+ }.test {
+ assertThat(awaitItem()).isTrue()
+ }
+ }
+}
diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/CoroutineDispatchers.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/CoroutineDispatchers.kt
index 94bfa8e324..606fe20158 100644
--- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/CoroutineDispatchers.kt
+++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/CoroutineDispatchers.kt
@@ -8,9 +8,18 @@
package io.element.android.libraries.core.coroutine
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
data class CoroutineDispatchers(
val io: CoroutineDispatcher,
val computation: CoroutineDispatcher,
val main: CoroutineDispatcher,
-)
+) {
+ companion object {
+ val Default = CoroutineDispatchers(
+ io = Dispatchers.IO,
+ computation = Dispatchers.Default,
+ main = Dispatchers.Main,
+ )
+ }
+}
diff --git a/libraries/dateformatter/impl/src/main/res/values-hu/translations.xml b/libraries/dateformatter/impl/src/main/res/values-hu/translations.xml
index 33778d84f1..0f4e767bf4 100644
--- a/libraries/dateformatter/impl/src/main/res/values-hu/translations.xml
+++ b/libraries/dateformatter/impl/src/main/res/values-hu/translations.xml
@@ -1,5 +1,5 @@
- "%1$s itt: %2$s"
+ "%1$s, %2$s"
"Ebben a hónapban"
diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts
index c2eec20b8c..3983317055 100644
--- a/libraries/designsystem/build.gradle.kts
+++ b/libraries/designsystem/build.gradle.kts
@@ -27,7 +27,7 @@ android {
}
dependencies {
- api(libs.compound)
+ api(projects.libraries.compound)
implementation(libs.androidx.compose.material3.windowsizeclass)
implementation(libs.androidx.compose.material3.adaptive)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/BetaLabel.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/BetaLabel.kt
new file mode 100644
index 0000000000..2600b83561
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/BetaLabel.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.libraries.designsystem.atomic.atoms
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.ui.strings.CommonStrings
+
+@Composable
+fun BetaLabel(
+ modifier: Modifier = Modifier,
+) {
+ val shape = RoundedCornerShape(size = 6.dp)
+ Text(
+ modifier = modifier
+ .border(
+ width = 1.dp,
+ color = ElementTheme.colors.borderInfoSubtle,
+ shape = shape,
+ )
+ .background(
+ color = ElementTheme.colors.bgInfoSubtle,
+ shape = shape,
+ )
+ .padding(horizontal = 8.dp, vertical = 4.dp),
+ text = stringResource(CommonStrings.common_beta).uppercase(),
+ style = ElementTheme.typography.fontBodySmMedium,
+ color = ElementTheme.colors.textInfoPrimary,
+ )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun BetaLabelPreview() = ElementPreview {
+ BetaLabel()
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt
index a89a21adbd..e38070e09f 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt
@@ -7,7 +7,9 @@
package io.element.android.libraries.designsystem.atomic.molecules
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -20,6 +22,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.libraries.designsystem.atomic.atoms.BetaLabel
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -32,6 +35,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
* @param subTitle the subtitle to display
* @param iconStyle the style of the [BigIcon] to display
* @param modifier the modifier to apply to this layout
+ * @param showBetaLabel whether to show a "BETA" label next to the title
*/
@Composable
fun IconTitleSubtitleMolecule(
@@ -39,6 +43,7 @@ fun IconTitleSubtitleMolecule(
subTitle: String?,
iconStyle: BigIcon.Style,
modifier: Modifier = Modifier,
+ showBetaLabel: Boolean = false,
) {
Column(modifier) {
BigIcon(
@@ -46,17 +51,26 @@ fun IconTitleSubtitleMolecule(
style = iconStyle,
)
Spacer(modifier = Modifier.height(16.dp))
- Text(
- text = title,
- modifier = Modifier
- .fillMaxWidth()
- .semantics {
- heading()
- },
- textAlign = TextAlign.Center,
- style = ElementTheme.typography.fontHeadingMdBold,
- color = ElementTheme.colors.textPrimary,
- )
+ FlowRow(
+ modifier = Modifier.fillMaxWidth(),
+ itemVerticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ Text(
+ text = title,
+ modifier = Modifier
+ .semantics {
+ heading()
+ },
+ textAlign = TextAlign.Center,
+ style = ElementTheme.typography.fontHeadingMdBold,
+ color = ElementTheme.colors.textPrimary,
+ )
+ if (showBetaLabel) {
+ BetaLabel()
+ }
+ }
if (subTitle != null) {
Spacer(Modifier.height(8.dp))
Text(
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt
index f469555ee4..78a4d32e44 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt
@@ -53,11 +53,13 @@ object BigIcon {
* @param vectorIcon the [ImageVector] to display
* @param contentDescription the content description of the icon, if any. It defaults to `null`
* @param useCriticalTint whether the icon and background should be rendered using critical tint
+ * @param usePrimaryTint whether the icon should be rendered using primary tint
*/
data class Default(
val vectorIcon: ImageVector,
val contentDescription: String? = null,
val useCriticalTint: Boolean = false,
+ val usePrimaryTint: Boolean = false,
) : Style
/**
@@ -143,6 +145,8 @@ object BigIcon {
val iconTint = when (style) {
is Style.Default -> if (style.useCriticalTint) {
ElementTheme.colors.iconCriticalPrimary
+ } else if (style.usePrimaryTint) {
+ ElementTheme.colors.iconPrimary
} else {
ElementTheme.colors.iconSecondary
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt
index 3638556da3..6c6d6d398e 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt
@@ -20,6 +20,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
@@ -51,6 +52,7 @@ fun ClickableLinkText(
onClick: () -> Unit = {},
onLongClick: () -> Unit = {},
style: TextStyle = LocalTextStyle.current,
+ color: Color = Color.Unspecified,
inlineContent: ImmutableMap = persistentMapOf(),
) {
ClickableLinkText(
@@ -62,6 +64,7 @@ fun ClickableLinkText(
onClick = onClick,
onLongClick = onLongClick,
style = style,
+ color = color,
inlineContent = inlineContent,
)
}
@@ -76,6 +79,7 @@ fun ClickableLinkText(
onClick: () -> Unit = {},
onLongClick: () -> Unit = {},
style: TextStyle = LocalTextStyle.current,
+ color: Color = Color.Unspecified,
inlineContent: ImmutableMap = persistentMapOf(),
) {
@Suppress("NAME_SHADOWING")
@@ -126,6 +130,7 @@ fun ClickableLinkText(
text = annotatedString,
modifier = modifier.then(pressIndicator),
style = style,
+ color = color,
onTextLayout = {
layoutResult.value = it
},
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/SimpleModalBottomSheet.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/SimpleModalBottomSheet.kt
new file mode 100644
index 0000000000..5b87b1171b
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/SimpleModalBottomSheet.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.designsystem.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
+import io.element.android.libraries.designsystem.theme.components.Text
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SimpleModalBottomSheet(
+ title: String,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ modifier = modifier,
+ sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ ) {
+ Text(
+ title,
+ style = ElementTheme.typography.fontBodyLgMedium,
+ color = ElementTheme.colors.textPrimary,
+ )
+ Spacer(Modifier.height(8.dp))
+ content()
+ }
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun SimpleModalBottomSheetPreview() = ElementPreview {
+ SimpleModalBottomSheet(title = "A title", onDismiss = {}) {
+ Text(
+ text = LoremIpsum(20).values.first(),
+ color = ElementTheme.colors.textSecondary,
+ style = ElementTheme.typography.fontBodyMdRegular,
+ )
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt
index db7ef62483..d3a9ed3107 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt
@@ -8,12 +8,13 @@
package io.element.android.libraries.designsystem.components.avatar
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.avatar.internal.RoomAvatar
@@ -21,8 +22,8 @@ import io.element.android.libraries.designsystem.components.avatar.internal.Spac
import io.element.android.libraries.designsystem.components.avatar.internal.UserAvatar
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
-import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
+import kotlinx.collections.immutable.persistentListOf
@Composable
fun Avatar(
@@ -64,18 +65,54 @@ fun Avatar(
@Preview(group = PreviewGroup.Avatars)
@Composable
-internal fun AvatarPreview(@PreviewParameter(AvatarDataProvider::class) avatarData: AvatarData) =
- ElementThemedPreview(
- drawableFallbackForImages = CommonDrawables.sample_avatar,
+internal fun AvatarPreview() = ElementThemedPreview(
+ drawableFallbackForImages = CommonDrawables.sample_background,
+) {
+ Column(
+ modifier = Modifier.padding(4.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- Avatar(
- avatarData = avatarData,
- avatarType = AvatarType.User,
- )
- Text(text = avatarData.size.name + " " + avatarData.size.dp)
+ listOf(
+ anAvatarData(size = AvatarSize.UserListItem),
+ anAvatarData(size = AvatarSize.UserListItem, name = null),
+ anAvatarData(size = AvatarSize.UserListItem, url = "aUrl"),
+ ).forEach { avatarData ->
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Avatar(
+ avatarData = avatarData,
+ avatarType = AvatarType.User,
+ )
+ Avatar(
+ avatarData = avatarData,
+ avatarType = AvatarType.Room(isTombstoned = false),
+ )
+ Avatar(
+ avatarData = avatarData,
+ avatarType = AvatarType.Room(
+ heroes = persistentListOf(
+ anAvatarData("@carol:server.org", "Carol", size = AvatarSize.UserListItem),
+ anAvatarData("@david:server.org", "David", size = AvatarSize.UserListItem),
+ anAvatarData("@eve:server.org", "Eve", size = AvatarSize.UserListItem),
+ anAvatarData("@justin:server.org", "Justin", size = AvatarSize.UserListItem),
+ )
+ )
+ )
+ Avatar(
+ avatarData = avatarData,
+ avatarType = AvatarType.Room(isTombstoned = true),
+ )
+ Avatar(
+ avatarData = avatarData,
+ avatarType = AvatarType.Space(isTombstoned = false),
+ )
+ Avatar(
+ avatarData = avatarData,
+ avatarType = AvatarType.Space(isTombstoned = true),
+ )
+ }
}
}
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt
index 7ccebd2082..2bc497f10d 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt
@@ -7,22 +7,6 @@
package io.element.android.libraries.designsystem.components.avatar
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-
-open class AvatarDataProvider : PreviewParameterProvider {
- override val values: Sequence
- get() = AvatarSize.entries
- .asSequence()
- .map {
- sequenceOf(
- anAvatarData(size = it),
- anAvatarData(size = it, name = null),
- anAvatarData(size = it, url = "aUrl"),
- )
- }
- .flatten()
-}
-
fun anAvatarData(
// Let's the id not start with a 'a'.
id: String = "@id_of_alice:server.org",
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt
index 72cf62c76a..31c569c870 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt
@@ -72,4 +72,7 @@ enum class AvatarSize(val dp: Dp) {
RoomPreviewHeader(64.dp),
RoomPreviewInviter(56.dp),
SpaceMember(24.dp),
+ LeaveSpaceRoom(32.dp),
+
+ AccountItem(32.dp),
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/AvatarCluster.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/AvatarCluster.kt
index c100988053..1c8ac81e64 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/AvatarCluster.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/AvatarCluster.kt
@@ -27,7 +27,7 @@ import io.element.android.libraries.designsystem.components.avatar.avatarShape
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import java.util.Collections
import kotlin.math.PI
import kotlin.math.cos
@@ -134,7 +134,7 @@ internal fun AvatarClusterPreview() = ElementThemedPreview {
) {
for (ngOfAvatars in 1..5) {
AvatarCluster(
- avatars = List(ngOfAvatars) { anAvatarData(it) }.toPersistentList(),
+ avatars = List(ngOfAvatars) { anAvatarData(it) }.toImmutableList(),
avatarType = avatarType,
)
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/RoomAvatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/RoomAvatar.kt
index 9fd2ff8cca..a9d1e5a95e 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/RoomAvatar.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/RoomAvatar.kt
@@ -13,6 +13,7 @@ import androidx.compose.ui.unit.Dp
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.avatar.avatarShape
+import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun RoomAvatar(
@@ -44,8 +45,9 @@ internal fun RoomAvatar(
}
else -> {
AvatarCluster(
- avatars = avatarType.heroes,
- // Note: even for a room avatar, we use UserAvatarType here to display the avatar of heroes
+ // Keep only the first hero for now
+ avatars = avatarType.heroes.take(1).toImmutableList(),
+ // Note: even for a room avatar, we use AvatarType.User here to display the avatar of heroes
avatarType = AvatarType.User,
modifier = modifier,
hideAvatarImages = hideAvatarImage,
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt
index c9e8876aad..3264f55754 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt
@@ -8,7 +8,7 @@
package io.element.android.libraries.designsystem.components.media
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlin.random.Random
/**
@@ -21,5 +21,5 @@ import kotlin.random.Random
fun createFakeWaveform(length: Int = 1000): ImmutableList {
val random = Random(seed = 2)
return List(length) { random.nextFloat() }
- .toPersistentList()
+ .toImmutableList()
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt
index 767b56410f..5fe722d447 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt
@@ -39,7 +39,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlin.math.roundToInt
private const val DEFAULT_GRAPHICS_LAYER_ALPHA: Float = 0.99F
@@ -187,14 +187,14 @@ internal fun WaveformPlaybackViewPreview() = ElementPreview {
showCursor = false,
playbackProgress = 0.5f,
onSeek = {},
- waveform = aWaveForm().toPersistentList(),
+ waveform = aWaveForm().toImmutableList(),
)
WaveformPlaybackView(
modifier = Modifier.height(34.dp),
showCursor = true,
playbackProgress = 0.5f,
onSeek = {},
- waveform = List(1024) { it / 1024f }.toPersistentList(),
+ waveform = List(1024) { it / 1024f }.toImmutableList(),
)
}
}
@@ -215,7 +215,7 @@ private fun ImmutableList.normalisedData(maxSamplesCount: Int): Immutable
this
}
- return result.toPersistentList()
+ return result.toImmutableList()
}
fun aWaveForm(): List {
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/icons/IconsPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/icons/IconsPreview.kt
index 993b2c32c4..e2da9bdcc0 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/icons/IconsPreview.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/icons/IconsPreview.kt
@@ -28,14 +28,14 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
internal class CompoundIconChunkProvider : PreviewParameterProvider {
override val values: Sequence
get() {
val chunks = CompoundIcons.allResIds.chunked(36)
return chunks.mapIndexed { index, chunk ->
- IconChunk(index = index + 1, total = chunks.size, icons = chunk.toPersistentList())
+ IconChunk(index = index + 1, total = chunks.size, icons = chunk.toImmutableList())
}
.asSequence()
}
@@ -46,7 +46,7 @@ internal class OtherIconChunkProvider : PreviewParameterProvider {
get() {
val chunks = iconsOther.chunked(36)
return chunks.mapIndexed { index, chunk ->
- IconChunk(index = index + 1, total = chunks.size, icons = chunk.toPersistentList())
+ IconChunk(index = index + 1, total = chunks.size, icons = chunk.toImmutableList())
}
.asSequence()
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt
index 3867e37c4a..f4487b34fa 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt
@@ -8,6 +8,11 @@
package io.element.android.libraries.designsystem.theme.components
import androidx.annotation.DrawableRes
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -15,9 +20,12 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
@@ -134,3 +142,24 @@ fun Icon(
internal fun IconImageVectorPreview() = ElementThemedPreview {
Icon(imageVector = CompoundIcons.Close(), contentDescription = null)
}
+
+@Preview(group = PreviewGroup.Icons)
+@Composable
+internal fun AllIconsPreview() = ElementPreview {
+ LazyVerticalGrid(
+ modifier = Modifier.fillMaxWidth(),
+ columns = GridCells.Adaptive(32.dp),
+ contentPadding = PaddingValues(2.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ horizontalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ CompoundIcons.allResIds.forEach { icon ->
+ item {
+ Icon(
+ painter = painterResource(icon),
+ contentDescription = null,
+ )
+ }
+ }
+ }
+}
diff --git a/libraries/di/build.gradle.kts b/libraries/di/build.gradle.kts
index a7bd4f2c44..1b2b04d880 100644
--- a/libraries/di/build.gradle.kts
+++ b/libraries/di/build.gradle.kts
@@ -11,6 +11,5 @@ plugins {
}
dependencies {
- api(libs.inject)
api(libs.metro.runtime)
}
diff --git a/libraries/eventformatter/impl/src/main/res/values-bg/translations.xml b/libraries/eventformatter/impl/src/main/res/values-bg/translations.xml
index b1f3fe1f11..9bfcdfe6a6 100644
--- a/libraries/eventformatter/impl/src/main/res/values-bg/translations.xml
+++ b/libraries/eventformatter/impl/src/main/res/values-bg/translations.xml
@@ -26,7 +26,11 @@
"%1$s получи достъп до %2$s"
"Вие позволихте на %1$s да се присъедини"
"Вие поискахте да се присъедините"
+ "%1$s отхвърли заявката на %2$s за присъединяване"
+ "Вие отхвърлихте заявката на %1$s за присъединяване"
+ "%1$s отхвърли вашата заявка за присъединяване"
"%1$s вече не се интересува от присъединяване"
+ "Вие отменихте заявката си за присъединяване"
"%1$s напусна стаята"
"Вие напуснахте стаята"
"%1$s промени името на стаята на: %2$s"
@@ -39,14 +43,19 @@
"Вие променихте закачените съобщения"
"%1$s закачи съобщение"
"Вие закачихте съобщение"
+ "%1$s откачи съобщение"
+ "Вие откачихте съобщение"
"%1$s отхвърли поканата"
"Вие отхвърлихте поканата"
"%1$s премахна %2$s"
"Вие премахнахте %1$s"
"%1$s изпрати покана на %2$s за присъединяване към стаята"
"Вие изпратихте покана на %1$s за присъединяване към стаята"
+ "%1$s отмени поканата на %2$s за присъединяване към стаята"
+ "Вие отменихте поканата на %1$s за присъединяване към стаята"
"%1$s промени темата на: %2$s"
"Вие променихте темата на: %1$s"
"%1$s премахна темата на стаята"
"Вие премахнахте темата на стаята"
+ "%1$s направи неизвестна промяна в членството си"
diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/Feature.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/Feature.kt
index af1031450e..18a9ea1bc3 100644
--- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/Feature.kt
+++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/Feature.kt
@@ -36,4 +36,10 @@ interface Feature {
* If true: the feature is finished, it will not appear in the developer options screen.
*/
val isFinished: Boolean
+
+ /**
+ * Whether the feature is only available in Labs (and not in developer options).
+ * Feature flags that set this to `true` can be enabled by any users, not only those that have enabled developer mode.
+ */
+ val isInLabs: Boolean
}
diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt
index 3bd94c9bd7..1358909771 100644
--- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt
+++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt
@@ -33,4 +33,9 @@ interface FeatureFlagService {
* is registered
*/
suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean
+
+ /**
+ * @return the list of available (not finished) features that can be toggled.
+ */
+ fun getAvailableFeatures(): List
}
diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
index 43895bce16..cb908dbe5e 100644
--- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
+++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
@@ -19,6 +19,7 @@ enum class FeatureFlags(
override val description: String? = null,
override val defaultValue: (BuildMeta) -> Boolean,
override val isFinished: Boolean,
+ override val isInLabs: Boolean = false,
) : Feature {
RoomDirectorySearch(
key = "feature.roomdirectorysearch",
@@ -71,8 +72,7 @@ enum class FeatureFlags(
Space(
key = "feature.space",
title = "Spaces",
- description = "Spaces are under active development, only developers should enable this flag for now.",
- defaultValue = { false },
+ defaultValue = { true },
isFinished = false,
),
PrintLogsToLogcat(
@@ -99,5 +99,14 @@ enum class FeatureFlags(
description = "Renders thread messages as a dedicated timeline. Restarting the app is required for this setting to fully take effect.",
defaultValue = { false },
isFinished = false,
- )
+ isInLabs = true,
+ ),
+ MultiAccount(
+ key = "feature.multi_account",
+ title = "Multi accounts",
+ description = "Allow the application to connect to multiple accounts at the same time." +
+ "\n\nWARNING: this feature is EXPERIMENTAL and UNSTABLE.",
+ defaultValue = { false },
+ isFinished = false,
+ ),
}
diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt
index 5bcbe93085..01c1e8a258 100644
--- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt
+++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt
@@ -14,6 +14,7 @@ import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.featureflag.api.Feature
import io.element.android.libraries.featureflag.api.FeatureFlagService
+import io.element.android.libraries.featureflag.api.FeatureFlags
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
@@ -40,4 +41,8 @@ class DefaultFeatureFlagService(
?.let { true }
?: false
}
+
+ override fun getAvailableFeatures(): List {
+ return FeatureFlags.entries.filter { !it.isFinished }
+ }
}
diff --git a/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeature.kt b/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeature.kt
new file mode 100644
index 0000000000..a141fbdd75
--- /dev/null
+++ b/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeature.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.featureflag.test
+
+import io.element.android.libraries.core.meta.BuildMeta
+import io.element.android.libraries.featureflag.api.Feature
+
+data class FakeFeature(
+ override val key: String,
+ override val title: String,
+ override val description: String? = null,
+ override val defaultValue: (BuildMeta) -> Boolean = { false },
+ override val isFinished: Boolean = false,
+ override val isInLabs: Boolean = false,
+) : Feature
diff --git a/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt b/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt
index 695d3014bc..49fc095ed4 100644
--- a/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt
+++ b/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt
@@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
class FakeFeatureFlagService(
initialState: Map = emptyMap(),
private val buildMeta: BuildMeta = aBuildMeta(),
+ var providedAvailableFeatures: List = emptyList(),
) : FeatureFlagService {
private val enabledFeatures = initialState
.mapValues { MutableStateFlow(it.value) }
@@ -31,4 +32,8 @@ class FakeFeatureFlagService(
override fun isFeatureEnabledFlow(feature: Feature): Flow {
return enabledFeatures.getOrPut(feature.key) { MutableStateFlow(feature.defaultValue(buildMeta)) }
}
+
+ override fun getAvailableFeatures(): List {
+ return providedAvailableFeatures
+ }
}
diff --git a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModel.kt b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModel.kt
index d6fa020f2f..f37e096c5b 100644
--- a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModel.kt
+++ b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModel.kt
@@ -7,9 +7,12 @@
package io.element.android.libraries.featureflag.ui.model
+import io.element.android.libraries.designsystem.theme.components.IconSource
+
data class FeatureUiModel(
val key: String,
val title: String,
val description: String?,
+ val icon: IconSource?,
val isEnabled: Boolean
)
diff --git a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModelProvider.kt b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModelProvider.kt
index 4d515eb984..66697b447f 100644
--- a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModelProvider.kt
+++ b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModelProvider.kt
@@ -12,7 +12,7 @@ import kotlinx.collections.immutable.persistentListOf
fun aFeatureUiModelList(): ImmutableList {
return persistentListOf(
- FeatureUiModel("key1", "Display State Events", "Show state events in the timeline", true),
- FeatureUiModel("key2", "Display Room Events", null, false),
+ FeatureUiModel(key = "key1", title = "Display State Events", description = "Show state events in the timeline", icon = null, isEnabled = true),
+ FeatureUiModel(key = "key2", title = "Display Room Events", description = null, icon = null, isEnabled = false),
)
}
diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt
index d3f9cf5971..b5b46ca847 100644
--- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt
+++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt
@@ -31,9 +31,8 @@ import org.maplibre.android.maps.Projection
*/
@Composable
public inline fun rememberCameraPositionState(
- key: String? = null,
crossinline init: CameraPositionState.() -> Unit = {}
-): CameraPositionState = rememberSaveable(key = key, saver = CameraPositionState.Saver) {
+): CameraPositionState = rememberSaveable(saver = CameraPositionState.Saver) {
CameraPositionState().apply(init)
}
diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLibreMap.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLibreMap.kt
index 5de213615e..f1ef0f0459 100644
--- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLibreMap.kt
+++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLibreMap.kt
@@ -8,7 +8,7 @@
package io.element.android.libraries.maplibre.compose
-import android.content.ComponentCallbacks
+import android.content.ComponentCallbacks2
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
@@ -235,11 +235,15 @@ private fun MapView.lifecycleObserver(previousState: MutableState
val roomListService: RoomListService
val spaceService: SpaceService
- val mediaLoader: MatrixMediaLoader
+ val syncService: SyncService
+ val sessionVerificationService: SessionVerificationService
+ val pushersService: PushersService
+ val notificationService: NotificationService
+ val notificationSettingsService: NotificationSettingsService
+ val encryptionService: EncryptionService
+ val roomDirectoryService: RoomDirectoryService
+ val mediaPreviewService: MediaPreviewService
+ val matrixMediaLoader: MatrixMediaLoader
val sessionCoroutineScope: CoroutineScope
val ignoredUsersFlow: StateFlow>
+ val roomMembershipObserver: RoomMembershipObserver
suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom?
suspend fun getRoom(roomId: RoomId): BaseRoom?
suspend fun findDM(userId: UserId): Result
@@ -68,14 +77,6 @@ interface MatrixClient {
suspend fun joinRoom(roomId: RoomId): Result
suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result
suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result
- fun syncService(): SyncService
- fun sessionVerificationService(): SessionVerificationService
- fun pushersService(): PushersService
- fun notificationService(): NotificationService
- fun notificationSettingsService(): NotificationSettingsService
- fun encryptionService(): EncryptionService
- fun roomDirectoryService(): RoomDirectoryService
- fun mediaPreviewService(): MediaPreviewService
suspend fun getCacheSize(): Long
/**
@@ -97,7 +98,6 @@ interface MatrixClient {
suspend fun getUserProfile(): Result
suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result
suspend fun uploadMedia(mimeType: String, data: ByteArray): Result
- fun roomMembershipObserver(): RoomMembershipObserver
/**
* Get a room info flow for a given room ID.
@@ -173,6 +173,16 @@ interface MatrixClient {
* Returns the maximum file upload size allowed by the Matrix server.
*/
suspend fun getMaxFileUploadSize(): Result
+
+ /**
+ * Returns the list of shared recent emoji reactions for this account.
+ */
+ suspend fun getRecentEmojis(): Result>
+
+ /**
+ * Adds an emoji to the list of recent emoji reactions for this account.
+ */
+ suspend fun addRecentEmoji(emoji: String): Result
}
/**
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt
index ef73edfaf5..03e8d57150 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt
@@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.api.auth
sealed class AuthenticationException(message: String) : Exception(message) {
+ class AccountAlreadyLoggedIn(userId: String) : AuthenticationException(userId)
class InvalidServerName(message: String) : AuthenticationException(message)
class SlidingSyncVersion(message: String) : AuthenticationException(message)
class Oidc(message: String) : AuthenticationException(message)
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt
index 26a030d361..c833d46718 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt
@@ -69,14 +69,6 @@ object MatrixPatterns {
str matches PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER
}
- /**
- * Tells if a string is a valid space id. This is an alias for [isRoomId]
- *
- * @param str the string to test
- * @return true if the string is a valid space Id
- */
- fun isSpaceId(str: String?) = isRoomId(str)
-
/**
* Tells if a string is a valid room id.
*
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt
index 74074ed8e4..6db907bacd 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt
@@ -7,23 +7,7 @@
package io.element.android.libraries.matrix.api.core
-import io.element.android.libraries.androidutils.metadata.isInDebug
-import java.io.Serializable
-
-@JvmInline
-value class SpaceId(val value: String) : Serializable {
- init {
- if (isInDebug && !MatrixPatterns.isSpaceId(value)) {
- error(
- "`$value` is not a valid space id.\n" +
- "Space ids are the same as room ids.\n" +
- "Example space id: `!space_id:domain`."
- )
- }
- }
-
- override fun toString(): String = value
-}
+typealias SpaceId = RoomId
/**
* Value to use when no space is selected by the user.
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt
index 1635cce375..961178ee3e 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt
@@ -17,6 +17,7 @@ interface EncryptionService {
val recoveryStateStateFlow: StateFlow
val enableRecoveryProgressStateFlow: StateFlow
val isLastDevice: StateFlow
+ val hasDevicesToVerifyAgainst: StateFlow
suspend fun enableBackups(): Result
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryException.kt
index 70b335d4f9..7dffbf3653 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryException.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryException.kt
@@ -11,6 +11,7 @@ import io.element.android.libraries.matrix.api.exception.ClientException
sealed class RecoveryException(message: String) : Exception(message) {
class SecretStorage(message: String) : RecoveryException(message)
+ class Import(message: String) : RecoveryException(message)
data object BackupExistsOnServer : RecoveryException("BackupExistsOnServer")
data class Client(val exception: ClientException) : RecoveryException(exception.message ?: "Unknown error")
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkData.kt
index 1f9dd8af8d..1f5f39dee7 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkData.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkData.kt
@@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.api.permalink
import android.net.Uri
+import android.os.Parcelable
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
@@ -15,13 +16,15 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
+import kotlinx.parcelize.Parcelize
/**
* This sealed class represents all the permalink cases.
* You don't have to instantiate yourself but should use [PermalinkParser] instead.
*/
@Immutable
-sealed interface PermalinkData {
+@Parcelize
+sealed interface PermalinkData : Parcelable {
data class RoomLink(
val roomIdOrAlias: RoomIdOrAlias,
val eventId: EventId? = null,
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/AddRecentEmoji.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/AddRecentEmoji.kt
new file mode 100644
index 0000000000..da657ea78a
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/AddRecentEmoji.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.api.recentemojis
+
+import dev.zacsweers.metro.Inject
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.matrix.api.MatrixClient
+import kotlinx.coroutines.withContext
+
+@Inject
+class AddRecentEmoji(
+ private val client: MatrixClient,
+ private val dispatchers: CoroutineDispatchers,
+) {
+ suspend operator fun invoke(emoji: String): Result = withContext(dispatchers.io) {
+ client.addRecentEmoji(emoji)
+ }
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/GetRecentEmojis.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/GetRecentEmojis.kt
new file mode 100644
index 0000000000..53adf88c37
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/GetRecentEmojis.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.api.recentemojis
+
+import dev.zacsweers.metro.ContributesBinding
+import dev.zacsweers.metro.Inject
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.MatrixClient
+import kotlinx.coroutines.withContext
+
+fun interface GetRecentEmojis {
+ suspend operator fun invoke(): Result>
+}
+
+@ContributesBinding(SessionScope::class)
+@Inject
+class DefaultGetRecentEmojis(
+ private val client: MatrixClient,
+ private val dispatchers: CoroutineDispatchers,
+) : GetRecentEmojis {
+ override suspend operator fun invoke(): Result> = withContext(dispatchers.io) {
+ client.getRecentEmojis()
+ }
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt
index 84aae82b66..2694191f89 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt
@@ -244,7 +244,9 @@ interface BaseRoom : Closeable {
suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow
- /**
+ suspend fun threadRootIdForEvent(eventId: EventId): Result
+
+ /**
* Destroy the room and release all resources associated to it.
*/
fun destroy()
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MessageEventType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MessageEventType.kt
index 5d20840cf7..0f2a61da6e 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MessageEventType.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MessageEventType.kt
@@ -7,28 +7,32 @@
package io.element.android.libraries.matrix.api.room
-enum class MessageEventType {
- CALL_ANSWER,
- CALL_INVITE,
- CALL_HANGUP,
- CALL_CANDIDATES,
- RTC_NOTIFICATION,
- KEY_VERIFICATION_READY,
- KEY_VERIFICATION_START,
- KEY_VERIFICATION_CANCEL,
- KEY_VERIFICATION_ACCEPT,
- KEY_VERIFICATION_KEY,
- KEY_VERIFICATION_MAC,
- KEY_VERIFICATION_DONE,
- REACTION,
- ROOM_ENCRYPTED,
- ROOM_MESSAGE,
- ROOM_REDACTION,
- STICKER,
- POLL_END,
- POLL_RESPONSE,
- POLL_START,
- UNSTABLE_POLL_END,
- UNSTABLE_POLL_RESPONSE,
- UNSTABLE_POLL_START,
+import androidx.compose.runtime.Immutable
+
+@Immutable
+sealed interface MessageEventType {
+ data object CallAnswer : MessageEventType
+ data object CallInvite : MessageEventType
+ data object CallHangup : MessageEventType
+ data object CallCandidates : MessageEventType
+ data object RtcNotification : MessageEventType
+ data object KeyVerificationReady : MessageEventType
+ data object KeyVerificationStart : MessageEventType
+ data object KeyVerificationCancel : MessageEventType
+ data object KeyVerificationAccept : MessageEventType
+ data object KeyVerificationKey : MessageEventType
+ data object KeyVerificationMac : MessageEventType
+ data object KeyVerificationDone : MessageEventType
+ data object Reaction : MessageEventType
+ data object RoomEncrypted : MessageEventType
+ data object RoomMessage : MessageEventType
+ data object RoomRedaction : MessageEventType
+ data object Sticker : MessageEventType
+ data object PollEnd : MessageEventType
+ data object PollResponse : MessageEventType
+ data object PollStart : MessageEventType
+ data object UnstablePollEnd : MessageEventType
+ data object UnstablePollResponse : MessageEventType
+ data object UnstablePollStart : MessageEventType
+ data class Other(val type: String) : MessageEventType
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt
index 19d7fdaaf2..a4a718b219 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt
@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.asSharedFlow
class RoomMembershipObserver {
data class RoomMembershipUpdate(
val roomId: RoomId,
+ val isSpace: Boolean,
val isUserInRoom: Boolean,
val change: MembershipChange,
)
@@ -22,12 +23,23 @@ class RoomMembershipObserver {
private val _updates = MutableSharedFlow(extraBufferCapacity = 10)
val updates = _updates.asSharedFlow()
- suspend fun notifyUserLeftRoom(roomId: RoomId, membershipBeforeLeft: CurrentUserMembership) {
+ suspend fun notifyUserLeftRoom(
+ roomId: RoomId,
+ isSpace: Boolean,
+ membershipBeforeLeft: CurrentUserMembership,
+ ) {
val membershipChange = when (membershipBeforeLeft) {
CurrentUserMembership.INVITED -> MembershipChange.INVITATION_REJECTED
CurrentUserMembership.KNOCKED -> MembershipChange.KNOCK_RETRACTED
else -> MembershipChange.LEFT
}
- _updates.emit(RoomMembershipUpdate(roomId, false, membershipChange))
+ _updates.emit(
+ RoomMembershipUpdate(
+ roomId = roomId,
+ isSpace = isSpace,
+ isUserInRoom = false,
+ change = membershipChange,
+ )
+ )
}
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomMembersWithRole.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomMembersWithRole.kt
index 30719626a8..689157f215 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomMembersWithRole.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomMembersWithRole.kt
@@ -12,7 +12,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.activeRoomMembers
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -38,7 +38,7 @@ fun BaseRoom.usersWithRole(role: RoomMember.Role): Flow
membersState.activeRoomMembers()
.filter { powerLevels.contains(it.userId) }
- .toPersistentList()
+ .toImmutableList()
}
.distinctUntilChanged()
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt
index 8b9b84eb18..405906a185 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt
@@ -58,11 +58,12 @@ sealed interface RoomListFilter {
data object Invite : RoomListFilter
/**
- * A filter that matches either Group or People rooms.
+ * A filter that matches either Group,People rooms or Space.
*/
sealed interface Category : RoomListFilter {
data object Group : Category
data object People : Category
+ data object Space : Category
}
/**
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/LeaveSpaceHandle.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/LeaveSpaceHandle.kt
new file mode 100644
index 0000000000..292a973dda
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/LeaveSpaceHandle.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.api.spaces
+
+import io.element.android.libraries.matrix.api.core.RoomId
+
+interface LeaveSpaceHandle {
+ /**
+ * The id of the space to leave.
+ */
+ val id: RoomId
+
+ /**
+ * Get a list of rooms that can be left when leaving the space.
+ * It will include the current space and all the subspaces and rooms that the user has joined.
+ */
+ suspend fun rooms(): Result>
+
+ /**
+ * Leave the space and the given rooms.
+ * If [roomIds] is empty, only the space will be left.
+ */
+ suspend fun leave(roomIds: List): Result
+
+ /**
+ * Close the handle and free resources.
+ */
+ fun close()
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/LeaveSpaceRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/LeaveSpaceRoom.kt
new file mode 100644
index 0000000000..fb90896e05
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/LeaveSpaceRoom.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.api.spaces
+
+data class LeaveSpaceRoom(
+ val spaceRoom: SpaceRoom,
+ val isLastAdmin: Boolean,
+)
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt
index d4e1d57826..3e72632e6c 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt
@@ -13,14 +13,16 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.user.MatrixUser
+import kotlinx.collections.immutable.ImmutableList
data class SpaceRoom(
- val name: String?,
+ val rawName: String?,
+ val displayName: String,
val avatarUrl: String?,
val canonicalAlias: RoomAlias?,
val childrenCount: Int,
val guestCanJoin: Boolean,
- val heroes: List,
+ val heroes: ImmutableList,
val joinRule: JoinRule?,
val numJoinedMembers: Int,
val roomId: RoomId,
@@ -28,6 +30,13 @@ data class SpaceRoom(
val state: CurrentUserMembership?,
val topic: String?,
val worldReadable: Boolean,
+ /**
+ * The via parameters of the room.
+ */
+ val via: ImmutableList,
+ val isDirect: Boolean?,
) {
val isSpace = roomType == RoomType.Space
+
+ val visibility = SpaceRoomVisibility.fromJoinRule(joinRule)
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.kt
index e55e1b87bd..1dddfadc5b 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.kt
@@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.api.spaces
+import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.util.Optional
@@ -17,9 +18,13 @@ interface SpaceRoomList {
data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus
}
- fun currentSpaceFlow(): StateFlow>
+ val roomId: RoomId
+
+ val currentSpaceFlow: StateFlow>
val spaceRoomsFlow: Flow>
val paginationStatusFlow: StateFlow
suspend fun paginate(): Result
+
+ fun destroy()
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomVisibility.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomVisibility.kt
new file mode 100644
index 0000000000..98afa1d508
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomVisibility.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.api.spaces
+
+import androidx.compose.runtime.Immutable
+import io.element.android.libraries.matrix.api.room.join.JoinRule
+@Immutable
+sealed interface SpaceRoomVisibility {
+ data object Private : SpaceRoomVisibility
+ data object Public : SpaceRoomVisibility
+ data object Restricted : SpaceRoomVisibility
+
+ companion object {
+ fun fromJoinRule(joinRule: JoinRule?): SpaceRoomVisibility = when (joinRule) {
+ JoinRule.Public -> Public
+ is JoinRule.Restricted, is JoinRule.KnockRestricted -> Restricted
+ // Else fallback to Private
+ else -> Private
+ }
+ }
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt
index b4572ad0bb..f1fea6b62a 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt
@@ -15,4 +15,6 @@ interface SpaceService {
suspend fun joinedSpaces(): Result>
fun spaceRoomList(id: RoomId): SpaceRoomList
+
+ fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentSessionIdHolder.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentSessionIdHolder.kt
deleted file mode 100644
index 171f8337e0..0000000000
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentSessionIdHolder.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.libraries.matrix.api.user
-
-import dev.zacsweers.metro.Inject
-import dev.zacsweers.metro.SingleIn
-import io.element.android.libraries.di.SessionScope
-import io.element.android.libraries.matrix.api.MatrixClient
-
-@SingleIn(SessionScope::class)
-@Inject
-class CurrentSessionIdHolder(matrixClient: MatrixClient) {
- val current = matrixClient.sessionId
-}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt
index 897e17c611..edf8c6ff78 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt
@@ -10,20 +10,14 @@ package io.element.android.libraries.matrix.api.verification
import android.os.Parcelable
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.FlowId
-import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.parcelize.Parcelize
@Parcelize
data class SessionVerificationRequestDetails(
- val senderProfile: SenderProfile,
+ val senderProfile: MatrixUser,
val flowId: FlowId,
val deviceId: DeviceId,
+ val deviceDisplayName: String?,
val firstSeenTimestamp: Long,
-) : Parcelable {
- @Parcelize
- data class SenderProfile(
- val userId: UserId,
- val displayName: String?,
- val avatarUrl: String?,
- ) : Parcelable
-}
+) : Parcelable
diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts
index 33ab700022..14ba9df71f 100644
--- a/libraries/matrix/impl/build.gradle.kts
+++ b/libraries/matrix/impl/build.gradle.kts
@@ -37,7 +37,7 @@ dependencies {
implementation(projects.services.toolbox.api)
api(projects.libraries.matrix.api)
implementation(projects.libraries.core)
- implementation("net.java.dev.jna:jna:5.17.0@aar")
+ implementation("net.java.dev.jna:jna:5.18.1@aar")
implementation(libs.androidx.datastore.preferences)
implementation(libs.serialization.json)
implementation(libs.kotlinx.collections.immutable)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
index 956e645571..c646e2ee18 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
@@ -23,13 +23,8 @@ 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.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.createroom.RoomPreset
-import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
-import io.element.android.libraries.matrix.api.media.MediaPreviewService
-import io.element.android.libraries.matrix.api.notification.NotificationService
-import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
-import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.JoinedRoom
@@ -39,18 +34,16 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.join.JoinRule
-import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
-import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
-import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService
import io.element.android.libraries.matrix.impl.exception.mapClientException
+import io.element.android.libraries.matrix.impl.mapper.map
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
import io.element.android.libraries.matrix.impl.media.RustMediaPreviewService
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
@@ -75,7 +68,6 @@ import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.libraries.matrix.impl.spaces.RustSpaceService
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.matrix.impl.sync.map
-import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
import io.element.android.libraries.matrix.impl.util.SessionPathsProvider
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
@@ -85,7 +77,7 @@ import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel
@@ -128,11 +120,10 @@ import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
class RustMatrixClient(
private val innerClient: Client,
- private val baseDirectory: File,
private val sessionStore: SessionStore,
- private val appCoroutineScope: CoroutineScope,
private val sessionDelegate: RustClientSessionDelegate,
private val innerSyncService: ClientSyncService,
+ appCoroutineScope: CoroutineScope,
dispatchers: CoroutineDispatchers,
baseCacheDirectory: File,
clock: SystemClock,
@@ -147,27 +138,29 @@ class RustMatrixClient(
private val innerRoomListService = innerSyncService.roomListService()
private val innerSpaceService = innerClient.spaceService()
- private val rustSyncService = RustSyncService(
+ override val roomMembershipObserver = RoomMembershipObserver()
+
+ override val syncService = RustSyncService(
inner = innerSyncService,
dispatcher = sessionDispatcher,
sessionCoroutineScope = sessionCoroutineScope
)
- private val pushersService = RustPushersService(
+ override val pushersService = RustPushersService(
client = innerClient,
dispatchers = dispatchers,
)
private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(innerSyncService)
private val innerNotificationClient = runBlocking { innerClient.notificationClient(notificationProcessSetup) }
- private val notificationService = RustNotificationService(sessionId, innerNotificationClient, dispatchers, clock)
- private val notificationSettingsService = RustNotificationSettingsService(innerClient, sessionCoroutineScope, dispatchers)
- private val encryptionService = RustEncryptionService(
+ override val notificationService = RustNotificationService(sessionId, innerNotificationClient, dispatchers, clock)
+ override val notificationSettingsService = RustNotificationSettingsService(innerClient, sessionCoroutineScope, dispatchers)
+ override val encryptionService = RustEncryptionService(
client = innerClient,
- syncService = rustSyncService,
+ syncService = syncService,
sessionCoroutineScope = sessionCoroutineScope,
dispatchers = dispatchers,
)
- private val roomDirectoryService = RustRoomDirectoryService(
+ override val roomDirectoryService = RustRoomDirectoryService(
client = innerClient,
sessionDispatcher = sessionDispatcher,
)
@@ -189,18 +182,19 @@ class RustMatrixClient(
override val spaceService: SpaceService = RustSpaceService(
innerSpaceService = innerSpaceService,
+ roomMembershipObserver = roomMembershipObserver,
sessionCoroutineScope = sessionCoroutineScope,
sessionDispatcher = sessionDispatcher,
)
- private val verificationService = RustSessionVerificationService(
+ override val sessionVerificationService = RustSessionVerificationService(
client = innerClient,
- isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running },
+ isSyncServiceReady = syncService.syncState.map { it == SyncState.Running },
sessionCoroutineScope = sessionCoroutineScope,
)
private val roomInfoMapper = RoomInfoMapper()
- private val roomMembershipObserver = RoomMembershipObserver()
+
private val roomFactory = RustRoomFactory(
roomListService = roomListService,
innerRoomListService = innerRoomListService,
@@ -218,13 +212,13 @@ class RustMatrixClient(
featureFlagService = featureFlagService,
)
- override val mediaLoader: MatrixMediaLoader = RustMediaLoader(
+ override val matrixMediaLoader: MatrixMediaLoader = RustMediaLoader(
baseCacheDirectory = baseCacheDirectory,
dispatchers = dispatchers,
innerClient = innerClient,
)
- private val mediaPreviewService = RustMediaPreviewService(
+ override val mediaPreviewService = RustMediaPreviewService(
sessionCoroutineScope = sessionCoroutineScope,
innerClient = innerClient,
sessionDispatcher = sessionDispatcher,
@@ -235,7 +229,6 @@ class RustMatrixClient(
private val _userProfile: MutableStateFlow = MutableStateFlow(
MatrixUser(
userId = sessionId,
- // TODO cache for displayName?
displayName = null,
avatarUrl = null,
)
@@ -245,11 +238,11 @@ class RustMatrixClient(
override val ignoredUsersFlow = mxCallbackFlow> {
// Fetch the initial value manually, the SDK won't return it automatically
- channel.trySend(innerClient.ignoredUsers().map(::UserId).toPersistentList())
+ channel.trySend(innerClient.ignoredUsers().map(::UserId).toImmutableList())
innerClient.subscribeToIgnoredUsers(object : IgnoredUsersListener {
override fun call(ignoredUserIds: List) {
- channel.trySend(ignoredUserIds.map(::UserId).toPersistentList())
+ channel.trySend(ignoredUserIds.map(::UserId).toImmutableList())
}
})
}
@@ -264,6 +257,16 @@ class RustMatrixClient(
// Start notification settings
notificationSettingsService.start()
+ // Update the user profile in the session store if needed
+ sessionStore.getSession(sessionId.value)?.let { sessionData ->
+ _userProfile.emit(
+ MatrixUser(
+ userId = sessionId,
+ displayName = sessionData.userDisplayName,
+ avatarUrl = sessionData.userAvatarUrl,
+ )
+ )
+ }
// Force a refresh of the profile
getUserProfile()
}
@@ -394,12 +397,20 @@ class RustMatrixClient(
override suspend fun getProfile(userId: UserId): Result = withContext(sessionDispatcher) {
runCatchingExceptions {
- innerClient.getProfile(userId.value).let(UserProfileMapper::map)
+ innerClient.getProfile(userId.value).map()
}
}
override suspend fun getUserProfile(): Result = getProfile(sessionId)
- .onSuccess { _userProfile.tryEmit(it) }
+ .onSuccess { matrixUser ->
+ _userProfile.emit(matrixUser)
+ // Also update our session storage
+ sessionStore.updateUserProfile(
+ sessionId = sessionId.value,
+ displayName = matrixUser.displayName,
+ avatarUrl = matrixUser.avatarUrl,
+ )
+ }
override suspend fun searchUsers(searchTerm: String, limit: Long): Result =
withContext(sessionDispatcher) {
@@ -518,33 +529,17 @@ class RustMatrixClient(
}.mapFailure { it.mapClientException() }
}
- override fun syncService(): SyncService = rustSyncService
-
- override fun sessionVerificationService(): SessionVerificationService = verificationService
-
- override fun pushersService(): PushersService = pushersService
-
- override fun notificationService(): NotificationService = notificationService
-
- override fun encryptionService(): EncryptionService = encryptionService
-
- override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService
-
- override fun roomDirectoryService(): RoomDirectoryService = roomDirectoryService
-
- override fun mediaPreviewService(): MediaPreviewService = mediaPreviewService
-
internal suspend fun destroy() {
innerNotificationClient.close()
roomFactory.destroy()
- rustSyncService.destroy()
+ syncService.destroy()
notificationSettingsService.destroy()
notificationProcessSetup.destroy()
sessionCoroutineScope.cancel()
clientDelegateTaskHandle?.cancelAndDestroy()
- verificationService.destroy()
+ sessionVerificationService.destroy()
sessionDelegate.clearCurrentClient()
innerRoomListService.close()
@@ -555,7 +550,7 @@ class RustMatrixClient(
}
override suspend fun getCacheSize(): Long {
- return baseDirectory.getCacheSize()
+ return getCacheSize(includeCryptoDb = false)
}
override suspend fun clearCache() {
@@ -654,8 +649,6 @@ class RustMatrixClient(
}
}
- override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
-
override fun getRoomInfoFlow(roomId: RoomId): Flow> {
return mxCallbackFlow {
val roomNotFound = innerRoomListService.roomOrNull(roomId.value).use { it == null }
@@ -708,7 +701,19 @@ class RustMatrixClient(
runCatchingExceptions { innerClient.getMaxMediaUploadSize().toLong() }
}
- private suspend fun File.getCacheSize(
+ override suspend fun addRecentEmoji(emoji: String): Result = withContext(sessionDispatcher) {
+ runCatchingExceptions {
+ innerClient.addRecentEmoji(emoji)
+ }
+ }
+
+ override suspend fun getRecentEmojis(): Result> = withContext(sessionDispatcher) {
+ runCatchingExceptions {
+ innerClient.getRecentEmojis().map { it.emoji }
+ }
+ }
+
+ private suspend fun getCacheSize(
includeCryptoDb: Boolean = false,
): Long = withContext(sessionDispatcher) {
val sessionDirectory = sessionPathsProvider.provides(sessionId) ?: return@withContext 0L
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
index 572bcbfd18..35b5bcf2b9 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
@@ -9,7 +9,6 @@ package io.element.android.libraries.matrix.impl
import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
-import io.element.android.libraries.di.BaseDirectory
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
@@ -43,7 +42,6 @@ import java.io.File
@Inject
class RustMatrixClientFactory(
- @BaseDirectory private val baseDirectory: File,
@CacheDirectory private val cacheDirectory: File,
@AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
@@ -87,7 +85,6 @@ class RustMatrixClientFactory(
return RustMatrixClient(
innerClient = client,
- baseDirectory = baseDirectory,
sessionStore = sessionStore,
appCoroutineScope = appCoroutineScope,
sessionDelegate = sessionDelegate,
@@ -136,13 +133,15 @@ class RustMatrixClientFactory(
)
.enableShareHistoryOnInvite(featureFlagService.isFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite))
.threadsEnabled(featureFlagService.isFeatureEnabled(FeatureFlags.Threads), threadSubscriptions = false)
- .requestConfig(RequestConfig(
- timeout = 30_000uL,
- retryLimit = 0u,
- // Use default values for the rest
- maxConcurrentRequests = null,
- maxRetryTime = null,
- ))
+ .requestConfig(
+ RequestConfig(
+ timeout = 30_000uL,
+ retryLimit = 0u,
+ // Use default values for the rest
+ maxConcurrentRequests = null,
+ maxRetryTime = null,
+ )
+ )
.run {
// Apply sliding sync version settings
when (slidingSyncType) {
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt
index 7175913dad..05eb4c4d5f 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt
@@ -14,6 +14,7 @@ import org.matrix.rustcomponents.sdk.OidcException
fun Throwable.mapAuthenticationException(): AuthenticationException {
val message = this.message ?: "Unknown error"
return when (this) {
+ is AuthenticationException -> this
is ClientBuildException -> when (this) {
is ClientBuildException.Generic -> AuthenticationException.Generic(message)
is ClientBuildException.InvalidServerName -> AuthenticationException.InvalidServerName(message)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
index 24201aaf63..88c86a43d6 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
@@ -15,6 +15,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.auth.AuthenticationException
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.auth.OidcDetails
@@ -139,6 +140,8 @@ class RustMatrixAuthenticationService(
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
client.login(username, password, "Element X Android", null)
+ // Ensure that the user is not already logged in with the same account
+ ensureNotAlreadyLoggedIn(client)
val sessionData = client.session()
.toSessionData(
isTokenValid = true,
@@ -227,17 +230,19 @@ class RustMatrixAuthenticationService(
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
client.loginWithOidcCallback(callbackUrl)
+
+ // Free the pending data since we won't use it to abort the flow anymore
+ pendingOAuthAuthorizationData?.close()
+ pendingOAuthAuthorizationData = null
+
+ // Ensure that the user is not already logged in with the same account
+ ensureNotAlreadyLoggedIn(client)
val sessionData = client.session().toSessionData(
isTokenValid = true,
loginType = LoginType.OIDC,
passphrase = pendingPassphrase,
sessionPaths = currentSessionPaths,
)
-
- // Free the pending data since we won't use it to abort the flow anymore
- pendingOAuthAuthorizationData?.close()
- pendingOAuthAuthorizationData = null
-
val matrixClient = rustMatrixClientFactory.create(client)
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
sessionStore.addSession(sessionData)
@@ -253,6 +258,21 @@ class RustMatrixAuthenticationService(
}
}
+ @Throws(AuthenticationException.AccountAlreadyLoggedIn::class)
+ private suspend fun ensureNotAlreadyLoggedIn(client: Client) {
+ val newUserId = client.userId()
+ val accountAlreadyLoggedIn = sessionStore.getAllSessions().any {
+ it.userId == newUserId
+ }
+ if (accountAlreadyLoggedIn) {
+ // Sign out the client, ignoring any error
+ runCatchingExceptions {
+ client.logout()
+ }
+ throw AuthenticationException.AccountAlreadyLoggedIn(newUserId)
+ }
+ }
+
override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) =
withContext(coroutineDispatchers.io) {
val sdkQrCodeLoginData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData
@@ -275,7 +295,8 @@ class RustMatrixAuthenticationService(
oidcConfiguration = oidcConfiguration,
progressListener = progressListener,
)
-
+ // Ensure that the user is not already logged in with the same account
+ ensureNotAlreadyLoggedIn(client)
val sessionData = client.session()
.toSessionData(
isTokenValid = true,
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt
index ef24cbba1a..37cf406f5f 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt
@@ -13,6 +13,7 @@ import dev.zacsweers.metro.Provides
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaPreviewService
@@ -20,6 +21,7 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
+import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import kotlinx.coroutines.CoroutineScope
@@ -27,19 +29,24 @@ import kotlinx.coroutines.CoroutineScope
@BindingContainer
@ContributesTo(SessionScope::class)
object SessionMatrixModule {
+ @Provides
+ fun providesSessionId(matrixClient: MatrixClient): SessionId {
+ return matrixClient.sessionId
+ }
+
@Provides
fun providesSessionVerificationService(matrixClient: MatrixClient): SessionVerificationService {
- return matrixClient.sessionVerificationService()
+ return matrixClient.sessionVerificationService
}
@Provides
fun providesNotificationSettingsService(matrixClient: MatrixClient): NotificationSettingsService {
- return matrixClient.notificationSettingsService()
+ return matrixClient.notificationSettingsService
}
@Provides
fun provideRoomMembershipObserver(matrixClient: MatrixClient): RoomMembershipObserver {
- return matrixClient.roomMembershipObserver()
+ return matrixClient.roomMembershipObserver
}
@Provides
@@ -49,32 +56,37 @@ object SessionMatrixModule {
@Provides
fun providesSyncService(matrixClient: MatrixClient): SyncService {
- return matrixClient.syncService()
+ return matrixClient.syncService
}
@Provides
fun providesEncryptionService(matrixClient: MatrixClient): EncryptionService {
- return matrixClient.encryptionService()
+ return matrixClient.encryptionService
}
@Provides
- fun provideMediaLoader(matrixClient: MatrixClient): MatrixMediaLoader {
- return matrixClient.mediaLoader
+ fun providesMatrixMediaLoader(matrixClient: MatrixClient): MatrixMediaLoader {
+ return matrixClient.matrixMediaLoader
}
@SessionCoroutineScope
@Provides
- fun provideSessionCoroutineScope(matrixClient: MatrixClient): CoroutineScope {
+ fun providesSessionCoroutineScope(matrixClient: MatrixClient): CoroutineScope {
return matrixClient.sessionCoroutineScope
}
@Provides
fun providesRoomDirectoryService(matrixClient: MatrixClient): RoomDirectoryService {
- return matrixClient.roomDirectoryService()
+ return matrixClient.roomDirectoryService
}
@Provides
fun providesMediaPreviewService(matrixClient: MatrixClient): MediaPreviewService {
- return matrixClient.mediaPreviewService()
+ return matrixClient.mediaPreviewService
+ }
+
+ @Provides
+ fun providesSpaceService(matrixClient: MatrixClient): SpaceService {
+ return matrixClient.spaceService
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryExceptionMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryExceptionMapper.kt
index b9679eb160..5568d008ec 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryExceptionMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryExceptionMapper.kt
@@ -20,6 +20,9 @@ fun Throwable.mapRecoveryException(): RecoveryException {
message = errorMessage
)
is RustRecoveryException.BackupExistsOnServer -> RecoveryException.BackupExistsOnServer
+ is RustRecoveryException.Import -> RecoveryException.Import(
+ message = errorMessage
+ )
is RustRecoveryException.Client -> RecoveryException.Client(
source.mapClientException()
)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
index 7c87666fe7..91cafd1df0 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
@@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.sync.SyncState
+import io.element.android.libraries.matrix.impl.exception.mapClientException
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
@@ -43,9 +44,10 @@ import org.matrix.rustcomponents.sdk.Encryption
import org.matrix.rustcomponents.sdk.UserIdentity
import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState
import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress
+import org.matrix.rustcomponents.sdk.RecoveryException as RustRecoveryException
import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException
-internal class RustEncryptionService(
+class RustEncryptionService(
client: Client,
syncService: RustSyncService,
sessionCoroutineScope: CoroutineScope,
@@ -96,6 +98,20 @@ internal class RustEncryptionService(
}
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
+ /**
+ * Check if the user has any devices available to verify against every 5 seconds.
+ * TODO This is a temporary workaround, when we will have a way to observe
+ * the sessions, this code will have to be updated.
+ */
+ override val hasDevicesToVerifyAgainst: StateFlow = flow {
+ while (currentCoroutineContext().isActive) {
+ val result = hasDevicesToVerifyAgainst().getOrDefault(false)
+ emit(result)
+ delay(5_000)
+ }
+ }
+ .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
+
override suspend fun enableBackups(): Result = withContext(dispatchers.io) {
runCatchingExceptions {
service.enableBackups()
@@ -171,6 +187,14 @@ internal class RustEncryptionService(
}
}
+ private suspend fun hasDevicesToVerifyAgainst(): Result = withContext(dispatchers.io) {
+ runCatchingExceptions {
+ service.hasDevicesToVerifyAgainst()
+ }.mapFailure {
+ it.mapClientException()
+ }
+ }
+
override suspend fun resetRecoveryKey(): Result = withContext(dispatchers.io) {
runCatchingExceptions {
service.resetRecoveryKey()
@@ -182,8 +206,12 @@ internal class RustEncryptionService(
override suspend fun recover(recoveryKey: String): Result = withContext(dispatchers.io) {
runCatchingExceptions {
service.recover(recoveryKey)
- }.mapFailure {
- it.mapRecoveryException()
+ }.recoverCatching {
+ when (it) {
+ // We ignore import errors because the user will be notified about them via the "Key storage out of sync" detection.
+ is RustRecoveryException.Import -> Unit
+ else -> throw it.mapRecoveryException()
+ }
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt
index 1d45c47470..2b5cac67ea 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt
@@ -34,6 +34,11 @@ internal fun Session.toSessionData(
passphrase = passphrase,
sessionPath = sessionPaths.fileDirectory.absolutePath,
cachePath = sessionPaths.cacheDirectory.absolutePath,
+ // Note: position and lastUsageIndex will be set by the SessionStore when adding the session
+ position = 0,
+ lastUsageIndex = 0,
+ userDisplayName = null,
+ userAvatarUrl = null,
)
internal fun ExternalSession.toSessionData(
@@ -55,4 +60,8 @@ internal fun ExternalSession.toSessionData(
passphrase = passphrase,
sessionPath = sessionPaths.fileDirectory.absolutePath,
cachePath = sessionPaths.cacheDirectory.absolutePath,
+ position = 0,
+ lastUsageIndex = 0,
+ userDisplayName = null,
+ userAvatarUrl = null,
)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserProfileMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/UserProfileMapper.kt
similarity index 53%
rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserProfileMapper.kt
rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/UserProfileMapper.kt
index f8f842c4be..6cc338610b 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserProfileMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/UserProfileMapper.kt
@@ -5,17 +5,14 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.libraries.matrix.impl.usersearch
+package io.element.android.libraries.matrix.impl.mapper
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import org.matrix.rustcomponents.sdk.UserProfile
-object UserProfileMapper {
- fun map(userProfile: UserProfile): MatrixUser =
- MatrixUser(
- userId = UserId(userProfile.userId),
- displayName = userProfile.displayName,
- avatarUrl = userProfile.avatarUrl,
- )
-}
+fun UserProfile.map() = MatrixUser(
+ userId = UserId(userId),
+ displayName = displayName,
+ avatarUrl = avatarUrl,
+)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventType.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventType.kt
index 4e88f0971e..14bd6d5e9d 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventType.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventType.kt
@@ -11,53 +11,55 @@ import io.element.android.libraries.matrix.api.room.MessageEventType
import org.matrix.rustcomponents.sdk.MessageLikeEventType
fun MessageEventType.map(): MessageLikeEventType = when (this) {
- MessageEventType.CALL_ANSWER -> MessageLikeEventType.CALL_ANSWER
- MessageEventType.CALL_INVITE -> MessageLikeEventType.CALL_INVITE
- MessageEventType.CALL_HANGUP -> MessageLikeEventType.CALL_HANGUP
- MessageEventType.CALL_CANDIDATES -> MessageLikeEventType.CALL_CANDIDATES
- MessageEventType.RTC_NOTIFICATION -> MessageLikeEventType.RTC_NOTIFICATION
- MessageEventType.KEY_VERIFICATION_READY -> MessageLikeEventType.KEY_VERIFICATION_READY
- MessageEventType.KEY_VERIFICATION_START -> MessageLikeEventType.KEY_VERIFICATION_START
- MessageEventType.KEY_VERIFICATION_CANCEL -> MessageLikeEventType.KEY_VERIFICATION_CANCEL
- MessageEventType.KEY_VERIFICATION_ACCEPT -> MessageLikeEventType.KEY_VERIFICATION_ACCEPT
- MessageEventType.KEY_VERIFICATION_KEY -> MessageLikeEventType.KEY_VERIFICATION_KEY
- MessageEventType.KEY_VERIFICATION_MAC -> MessageLikeEventType.KEY_VERIFICATION_MAC
- MessageEventType.KEY_VERIFICATION_DONE -> MessageLikeEventType.KEY_VERIFICATION_DONE
- MessageEventType.REACTION -> MessageLikeEventType.REACTION
- MessageEventType.ROOM_ENCRYPTED -> MessageLikeEventType.ROOM_ENCRYPTED
- MessageEventType.ROOM_MESSAGE -> MessageLikeEventType.ROOM_MESSAGE
- MessageEventType.ROOM_REDACTION -> MessageLikeEventType.ROOM_REDACTION
- MessageEventType.STICKER -> MessageLikeEventType.STICKER
- MessageEventType.POLL_END -> MessageLikeEventType.POLL_END
- MessageEventType.POLL_RESPONSE -> MessageLikeEventType.POLL_RESPONSE
- MessageEventType.POLL_START -> MessageLikeEventType.POLL_START
- MessageEventType.UNSTABLE_POLL_END -> MessageLikeEventType.UNSTABLE_POLL_END
- MessageEventType.UNSTABLE_POLL_RESPONSE -> MessageLikeEventType.UNSTABLE_POLL_RESPONSE
- MessageEventType.UNSTABLE_POLL_START -> MessageLikeEventType.UNSTABLE_POLL_START
+ MessageEventType.CallAnswer -> MessageLikeEventType.CallAnswer
+ MessageEventType.CallInvite -> MessageLikeEventType.CallInvite
+ MessageEventType.CallHangup -> MessageLikeEventType.CallHangup
+ MessageEventType.CallCandidates -> MessageLikeEventType.CallCandidates
+ MessageEventType.RtcNotification -> MessageLikeEventType.RtcNotification
+ MessageEventType.KeyVerificationReady -> MessageLikeEventType.KeyVerificationReady
+ MessageEventType.KeyVerificationStart -> MessageLikeEventType.KeyVerificationStart
+ MessageEventType.KeyVerificationCancel -> MessageLikeEventType.KeyVerificationCancel
+ MessageEventType.KeyVerificationAccept -> MessageLikeEventType.KeyVerificationAccept
+ MessageEventType.KeyVerificationKey -> MessageLikeEventType.KeyVerificationKey
+ MessageEventType.KeyVerificationMac -> MessageLikeEventType.KeyVerificationMac
+ MessageEventType.KeyVerificationDone -> MessageLikeEventType.KeyVerificationDone
+ MessageEventType.Reaction -> MessageLikeEventType.Reaction
+ MessageEventType.RoomEncrypted -> MessageLikeEventType.RoomEncrypted
+ MessageEventType.RoomMessage -> MessageLikeEventType.RoomMessage
+ MessageEventType.RoomRedaction -> MessageLikeEventType.RoomRedaction
+ MessageEventType.Sticker -> MessageLikeEventType.Sticker
+ MessageEventType.PollEnd -> MessageLikeEventType.PollEnd
+ MessageEventType.PollResponse -> MessageLikeEventType.PollResponse
+ MessageEventType.PollStart -> MessageLikeEventType.PollStart
+ MessageEventType.UnstablePollEnd -> MessageLikeEventType.UnstablePollEnd
+ MessageEventType.UnstablePollResponse -> MessageLikeEventType.UnstablePollResponse
+ MessageEventType.UnstablePollStart -> MessageLikeEventType.UnstablePollStart
+ is MessageEventType.Other -> MessageLikeEventType.Other(type)
}
fun MessageLikeEventType.map(): MessageEventType = when (this) {
- MessageLikeEventType.CALL_ANSWER -> MessageEventType.CALL_ANSWER
- MessageLikeEventType.CALL_INVITE -> MessageEventType.CALL_INVITE
- MessageLikeEventType.CALL_HANGUP -> MessageEventType.CALL_HANGUP
- MessageLikeEventType.CALL_CANDIDATES -> MessageEventType.CALL_CANDIDATES
- MessageLikeEventType.RTC_NOTIFICATION -> MessageEventType.RTC_NOTIFICATION
- MessageLikeEventType.KEY_VERIFICATION_READY -> MessageEventType.KEY_VERIFICATION_READY
- MessageLikeEventType.KEY_VERIFICATION_START -> MessageEventType.KEY_VERIFICATION_START
- MessageLikeEventType.KEY_VERIFICATION_CANCEL -> MessageEventType.KEY_VERIFICATION_CANCEL
- MessageLikeEventType.KEY_VERIFICATION_ACCEPT -> MessageEventType.KEY_VERIFICATION_ACCEPT
- MessageLikeEventType.KEY_VERIFICATION_KEY -> MessageEventType.KEY_VERIFICATION_KEY
- MessageLikeEventType.KEY_VERIFICATION_MAC -> MessageEventType.KEY_VERIFICATION_MAC
- MessageLikeEventType.KEY_VERIFICATION_DONE -> MessageEventType.KEY_VERIFICATION_DONE
- MessageLikeEventType.REACTION -> MessageEventType.REACTION
- MessageLikeEventType.ROOM_ENCRYPTED -> MessageEventType.ROOM_ENCRYPTED
- MessageLikeEventType.ROOM_MESSAGE -> MessageEventType.ROOM_MESSAGE
- MessageLikeEventType.ROOM_REDACTION -> MessageEventType.ROOM_REDACTION
- MessageLikeEventType.STICKER -> MessageEventType.STICKER
- MessageLikeEventType.POLL_END -> MessageEventType.POLL_END
- MessageLikeEventType.POLL_RESPONSE -> MessageEventType.POLL_RESPONSE
- MessageLikeEventType.POLL_START -> MessageEventType.POLL_START
- MessageLikeEventType.UNSTABLE_POLL_END -> MessageEventType.UNSTABLE_POLL_END
- MessageLikeEventType.UNSTABLE_POLL_RESPONSE -> MessageEventType.UNSTABLE_POLL_RESPONSE
- MessageLikeEventType.UNSTABLE_POLL_START -> MessageEventType.UNSTABLE_POLL_START
+ MessageLikeEventType.CallAnswer -> MessageEventType.CallAnswer
+ MessageLikeEventType.CallInvite -> MessageEventType.CallInvite
+ MessageLikeEventType.CallHangup -> MessageEventType.CallHangup
+ MessageLikeEventType.CallCandidates -> MessageEventType.CallCandidates
+ MessageLikeEventType.RtcNotification -> MessageEventType.RtcNotification
+ MessageLikeEventType.KeyVerificationReady -> MessageEventType.KeyVerificationReady
+ MessageLikeEventType.KeyVerificationStart -> MessageEventType.KeyVerificationStart
+ MessageLikeEventType.KeyVerificationCancel -> MessageEventType.KeyVerificationCancel
+ MessageLikeEventType.KeyVerificationAccept -> MessageEventType.KeyVerificationAccept
+ MessageLikeEventType.KeyVerificationKey -> MessageEventType.KeyVerificationKey
+ MessageLikeEventType.KeyVerificationMac -> MessageEventType.KeyVerificationMac
+ MessageLikeEventType.KeyVerificationDone -> MessageEventType.KeyVerificationDone
+ MessageLikeEventType.Reaction -> MessageEventType.Reaction
+ MessageLikeEventType.RoomEncrypted -> MessageEventType.RoomEncrypted
+ MessageLikeEventType.RoomMessage -> MessageEventType.RoomMessage
+ MessageLikeEventType.RoomRedaction -> MessageEventType.RoomRedaction
+ MessageLikeEventType.Sticker -> MessageEventType.Sticker
+ MessageLikeEventType.PollEnd -> MessageEventType.PollEnd
+ MessageLikeEventType.PollResponse -> MessageEventType.PollResponse
+ MessageLikeEventType.PollStart -> MessageEventType.PollStart
+ MessageLikeEventType.UnstablePollEnd -> MessageEventType.UnstablePollEnd
+ MessageLikeEventType.UnstablePollResponse -> MessageEventType.UnstablePollResponse
+ MessageLikeEventType.UnstablePollStart -> MessageEventType.UnstablePollStart
+ is MessageLikeEventType.Other -> MessageEventType.Other(v1)
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapper.kt
index 199abcad44..4339b69101 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapper.kt
@@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsValuesMapper
import io.element.android.libraries.matrix.impl.room.tombstone.map
import kotlinx.collections.immutable.toImmutableList
-import kotlinx.collections.immutable.toPersistentMap
+import kotlinx.collections.immutable.toImmutableMap
import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.RoomHero
import uniffi.matrix_sdk_base.EncryptionState
@@ -103,6 +103,6 @@ fun RoomHero.map(): MatrixUser = MatrixUser(
fun mapPowerLevels(roomPowerLevels: RustRoomPowerLevels): RoomPowerLevels {
return RoomPowerLevels(
values = RoomPowerLevelsValuesMapper.map(roomPowerLevels.values()),
- users = roomPowerLevels.userPowerLevels().mapKeys { (key, _) -> UserId(key) }.toPersistentMap()
+ users = roomPowerLevels.userPowerLevels().mapKeys { (key, _) -> UserId(key) }.toImmutableMap()
)
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt
index 1ca5915c71..6061ebd79c 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt
@@ -157,7 +157,11 @@ class RustBaseRoom(
runCatchingExceptions {
innerRoom.leave()
}.onSuccess {
- roomMembershipObserver.notifyUserLeftRoom(roomId, membershipBeforeLeft)
+ roomMembershipObserver.notifyUserLeftRoom(
+ roomId = roomId,
+ isSpace = roomInfoFlow.value.isSpace,
+ membershipBeforeLeft = membershipBeforeLeft,
+ )
}
}
@@ -318,4 +322,12 @@ class RustBaseRoom(
})
}
}
+
+ override suspend fun threadRootIdForEvent(eventId: EventId): Result = withContext(roomDispatcher) {
+ runCatchingExceptions {
+ innerRoom.loadOrFetchEvent(eventId.value).use {
+ it.threadRootEventId()?.let(::ThreadId)
+ }
+ }
+ }
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt
index 51422787e2..e145cbdb91 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt
@@ -8,7 +8,7 @@
package io.element.android.libraries.matrix.impl.room.join
import io.element.android.libraries.matrix.api.room.join.JoinRule
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule
fun RustJoinRule.map(): JoinRule {
@@ -17,9 +17,9 @@ fun RustJoinRule.map(): JoinRule {
RustJoinRule.Private -> JoinRule.Private
RustJoinRule.Knock -> JoinRule.Knock
RustJoinRule.Invite -> JoinRule.Invite
- is RustJoinRule.Restricted -> JoinRule.Restricted(rules.map { it.map() }.toPersistentList())
+ is RustJoinRule.Restricted -> JoinRule.Restricted(rules.map { it.map() }.toImmutableList())
is RustJoinRule.Custom -> JoinRule.Custom(repr)
- is RustJoinRule.KnockRestricted -> JoinRule.KnockRestricted(rules.map { it.map() }.toPersistentList())
+ is RustJoinRule.KnockRestricted -> JoinRule.KnockRestricted(rules.map { it.map() }.toImmutableList())
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt
index 15ba644fcf..53b8127ab8 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt
@@ -29,7 +29,6 @@ import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
private val ROOM_LIST_RUST_FILTERS = listOf(
RoomListEntriesDynamicFilterKind.NonLeft,
- RoomListEntriesDynamicFilterKind.NonSpace,
RoomListEntriesDynamicFilterKind.DeduplicateVersions
)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt
index 1e4a3fad3e..3fc59d7c44 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt
@@ -15,41 +15,57 @@ import io.element.android.libraries.matrix.api.roomlist.RoomSummary
val RoomListFilter.predicate
get() = when (this) {
- is RoomListFilter.All -> { _: RoomSummary -> true }
- is RoomListFilter.Any -> { _: RoomSummary -> true }
- RoomListFilter.None -> { _: RoomSummary -> false }
+ is RoomListFilter.All -> { roomSummary -> NonSpacePredicate(roomSummary) || IsInvitedPredicate(roomSummary) }
+ is RoomListFilter.Any -> { roomSummary -> NonSpacePredicate(roomSummary) || IsInvitedPredicate(roomSummary) }
+ RoomListFilter.None -> { _ -> false }
RoomListFilter.Category.Group -> { roomSummary: RoomSummary ->
- !roomSummary.info.isDm && !roomSummary.isInvited()
+ !roomSummary.info.isDm && NonInvitedPredicate(roomSummary) && NonSpacePredicate(roomSummary)
}
RoomListFilter.Category.People -> { roomSummary: RoomSummary ->
- roomSummary.info.isDm && !roomSummary.isInvited()
+ roomSummary.info.isDm && NonInvitedPredicate(roomSummary) && NonSpacePredicate(roomSummary)
}
+ RoomListFilter.Category.Space -> IsSpacePredicate
RoomListFilter.Favorite -> { roomSummary: RoomSummary ->
- roomSummary.info.isFavorite && !roomSummary.isInvited()
+ roomSummary.info.isFavorite && NonInvitedPredicate(roomSummary) && NonSpacePredicate(roomSummary)
}
RoomListFilter.Unread -> { roomSummary: RoomSummary ->
- !roomSummary.isInvited() && (roomSummary.info.numUnreadNotifications > 0 || roomSummary.info.isMarkedUnread)
+ NonInvitedPredicate(roomSummary) &&
+ NonSpacePredicate(roomSummary) &&
+ (roomSummary.info.numUnreadNotifications > 0 || roomSummary.info.isMarkedUnread)
}
is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary ->
- roomSummary.info.name?.withoutAccents().orEmpty().contains(normalizedPattern, ignoreCase = true)
- }
- RoomListFilter.Invite -> { roomSummary: RoomSummary ->
- roomSummary.isInvited()
+ roomSummary.info.name?.withoutAccents().orEmpty().contains(normalizedPattern, ignoreCase = true) &&
+ (NonSpacePredicate(roomSummary) || IsInvitedPredicate(roomSummary))
}
+ RoomListFilter.Invite -> IsInvitedPredicate
}
fun List.filter(filter: RoomListFilter): List {
return when (filter) {
is RoomListFilter.All -> {
- val predicates = filter.filters.map { it.predicate }
+ val predicates = if (filter.filters.isNotEmpty()) {
+ filter.filters.map { it.predicate }
+ } else {
+ listOf(filter.predicate)
+ }
filter { roomSummary -> predicates.all { it(roomSummary) } }
}
is RoomListFilter.Any -> {
- val predicates = filter.filters.map { it.predicate }
+ val predicates = if (filter.filters.isNotEmpty()) {
+ filter.filters.map { it.predicate }
+ } else {
+ listOf(filter.predicate)
+ }
filter { roomSummary -> predicates.any { it(roomSummary) } }
}
else -> filter(filter.predicate)
}
}
-private fun RoomSummary.isInvited() = info.currentUserMembership == CurrentUserMembership.INVITED
+private val IsSpacePredicate = { roomSummary: RoomSummary -> roomSummary.info.isSpace }
+
+private val NonSpacePredicate = { roomSummary: RoomSummary -> !IsSpacePredicate(roomSummary) }
+
+private val IsInvitedPredicate = { roomSummary: RoomSummary -> roomSummary.info.currentUserMembership == CurrentUserMembership.INVITED }
+
+private val NonInvitedPredicate = { roomSummary: RoomSummary -> !IsInvitedPredicate(roomSummary) }
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustLeaveSpaceHandle.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustLeaveSpaceHandle.kt
new file mode 100644
index 0000000000..e7e629b40b
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustLeaveSpaceHandle.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.impl.spaces
+
+import io.element.android.libraries.core.extensions.runCatchingExceptions
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.room.CurrentUserMembership
+import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
+import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
+import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import org.matrix.rustcomponents.sdk.LeaveSpaceHandle as RustLeaveSpaceHandle
+
+class RustLeaveSpaceHandle(
+ override val id: RoomId,
+ private val spaceRoomMapper: SpaceRoomMapper,
+ private val roomMembershipObserver: RoomMembershipObserver,
+ sessionCoroutineScope: CoroutineScope,
+ private val innerProvider: suspend () -> RustLeaveSpaceHandle,
+) : LeaveSpaceHandle {
+ private val inner = CompletableDeferred()
+
+ init {
+ sessionCoroutineScope.launch {
+ inner.complete(innerProvider())
+ }
+ }
+
+ override suspend fun rooms(): Result> = runCatchingExceptions {
+ inner.await().rooms().map { leaveSpaceRoom ->
+ LeaveSpaceRoom(
+ spaceRoom = spaceRoomMapper.map(leaveSpaceRoom.spaceRoom),
+ isLastAdmin = leaveSpaceRoom.isLastAdmin,
+ )
+ }
+ }
+
+ override suspend fun leave(roomIds: List): Result = runCatchingExceptions {
+ // Ensure the space is included and is the last room to be left
+ val roomToLeave = roomIds - id + id
+ inner.await().leave(roomToLeave.map { it.value })
+ }.onSuccess {
+ roomMembershipObserver.notifyUserLeftRoom(
+ roomId = id,
+ isSpace = true,
+ membershipBeforeLeft = CurrentUserMembership.JOINED,
+ )
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override fun close() {
+ Timber.d("Destroying LeaveSpaceHandle $id")
+ try {
+ inner.getCompleted().destroy()
+ } catch (_: Exception) {
+ // Ignore, we just want to make sure it's completed
+ }
+ }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceRoomList.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceRoomList.kt
index 1a940bd2f0..b94c3ffd1b 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceRoomList.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceRoomList.kt
@@ -13,62 +13,78 @@ import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
+import timber.log.Timber
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
import java.util.Optional
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
class RustSpaceRoomList(
- private val roomId: RoomId,
+ override val roomId: RoomId,
private val innerProvider: suspend () -> InnerSpaceRoomList,
- sessionCoroutineScope: CoroutineScope,
+ private val coroutineScope: CoroutineScope,
spaceRoomMapper: SpaceRoomMapper,
- private val spaceRoomCache: SpaceRoomCache,
) : SpaceRoomList {
- private val inner = CompletableDeferred()
+ private val innerCompletable = CompletableDeferred()
- override fun currentSpaceFlow(): StateFlow> {
- return spaceRoomCache.getSpaceRoomFlow(roomId)
- }
+ override val currentSpaceFlow = MutableStateFlow>(Optional.empty())
override val spaceRoomsFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
+
override val paginationStatusFlow: MutableStateFlow =
MutableStateFlow(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false))
private val spaceListUpdateProcessor = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow,
- mapper = spaceRoomMapper,
- spaceRoomCache = spaceRoomCache
+ mapper = spaceRoomMapper
)
init {
- sessionCoroutineScope.launch {
- inner.complete(innerProvider())
- }
- sessionCoroutineScope.launch {
- inner.await().paginationStateFlow()
+ coroutineScope.launch {
+ val inner = innerProvider()
+ innerCompletable.complete(inner)
+
+ inner.paginationStateFlow()
.onEach { paginationStatus ->
paginationStatusFlow.emit(paginationStatus.into())
}
- .collect()
- }
+ .launchIn(this)
- sessionCoroutineScope.launch {
- inner.await().spaceListUpdateFlow()
+ inner.spaceListUpdateFlow()
.onEach { updates ->
spaceListUpdateProcessor.postUpdates(updates)
}
- .collect()
+ .launchIn(this)
+
+ inner.spaceUpdateFlow()
+ .map { space -> space.map(spaceRoomMapper::map) }
+ .onEach { space ->
+ currentSpaceFlow.emit(space)
+ }
+ .launchIn(this)
}
}
override suspend fun paginate(): Result {
return runCatchingExceptions {
- inner.await().paginate()
+ innerCompletable.await().paginate()
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override fun destroy() {
+ Timber.d("Destroying SpaceRoomList $roomId")
+ coroutineScope.cancel()
+ try {
+ innerCompletable.getCompleted().destroy()
+ } catch (_: Exception) {
+ // Ignore, we just want to make sure it's completed
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt
index 2a63367577..88f738f0c4 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt
@@ -7,8 +7,11 @@
package io.element.android.libraries.matrix.impl.spaces
+import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
+import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.api.spaces.SpaceService
@@ -36,14 +39,13 @@ class RustSpaceService(
private val innerSpaceService: ClientSpaceService,
private val sessionCoroutineScope: CoroutineScope,
private val sessionDispatcher: CoroutineDispatcher,
+ private val roomMembershipObserver: RoomMembershipObserver,
) : SpaceService {
private val spaceRoomMapper = SpaceRoomMapper()
- private val spaceRoomCache = SpaceRoomCache()
override val spaceRoomsFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1)
private val spaceListUpdateProcessor = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow,
- mapper = spaceRoomMapper,
- spaceRoomCache = spaceRoomCache
+ mapper = spaceRoomMapper
)
override suspend fun joinedSpaces(): Result> = withContext(sessionDispatcher) {
@@ -56,15 +58,26 @@ class RustSpaceService(
}
override fun spaceRoomList(id: RoomId): SpaceRoomList {
+ val childCoroutineScope = sessionCoroutineScope.childScope(sessionDispatcher, "SpaceRoomListScope-$this")
return RustSpaceRoomList(
roomId = id,
innerProvider = { innerSpaceService.spaceRoomList(id.value) },
- sessionCoroutineScope = sessionCoroutineScope,
+ coroutineScope = childCoroutineScope,
spaceRoomMapper = spaceRoomMapper,
- spaceRoomCache = spaceRoomCache,
)
}
+ override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
+ return RustLeaveSpaceHandle(
+ id = spaceId,
+ spaceRoomMapper = spaceRoomMapper,
+ roomMembershipObserver = roomMembershipObserver,
+ sessionCoroutineScope = sessionCoroutineScope,
+ ) {
+ innerSpaceService.leaveSpace(spaceId.value)
+ }
+ }
+
init {
innerSpaceService
.spaceListUpdate()
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceListUpdateProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceListUpdateProcessor.kt
index f1193661ea..40310561e2 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceListUpdateProcessor.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceListUpdateProcessor.kt
@@ -18,7 +18,6 @@ import timber.log.Timber
internal class SpaceListUpdateProcessor(
private val spaceRoomsFlow: MutableSharedFlow>,
private val mapper: SpaceRoomMapper,
- private val spaceRoomCache: SpaceRoomCache,
) {
private val mutex = Mutex()
@@ -37,7 +36,6 @@ internal class SpaceListUpdateProcessor(
mutableListOf()
}
block(spaceRooms)
- spaceRoomCache.update(spaceRooms)
spaceRoomsFlow.emit(spaceRooms)
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomCache.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomCache.kt
deleted file mode 100644
index c8f55f1db6..0000000000
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomCache.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 2025 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.libraries.matrix.impl.spaces
-
-import io.element.android.libraries.core.coroutine.mapState
-import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.spaces.SpaceRoom
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.update
-import java.util.Optional
-
-/**
- * An in memory cache of space rooms.
- * Only caches Rooms with roomType [io.element.android.libraries.matrix.api.room.RoomType.Space].
- */
-class SpaceRoomCache {
- private val inMemoryCache = MutableStateFlow