diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index da1299b035..eaea9a841d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -31,7 +31,7 @@ jobs:
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
diff --git a/.github/workflows/build_enterprise.yml b/.github/workflows/build_enterprise.yml
index bccdb84516..9c4c8cec8f 100644
--- a/.github/workflows/build_enterprise.yml
+++ b/.github/workflows/build_enterprise.yml
@@ -39,7 +39,7 @@ jobs:
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
diff --git a/.github/workflows/generate_github_pages.yml b/.github/workflows/generate_github_pages.yml
index 5fddd72eea..35953ba661 100644
--- a/.github/workflows/generate_github_pages.yml
+++ b/.github/workflows/generate_github_pages.yml
@@ -14,7 +14,7 @@ jobs:
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
diff --git a/.github/workflows/gradle-wrapper-update.yml b/.github/workflows/gradle-wrapper-update.yml
index a9cb463629..f826bb8205 100644
--- a/.github/workflows/gradle-wrapper-update.yml
+++ b/.github/workflows/gradle-wrapper-update.yml
@@ -12,7 +12,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@v5
- - uses: actions/setup-java@v4
+ - uses: actions/setup-java@v5
name: Use JDK 21
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
diff --git a/.github/workflows/maestro-local.yml b/.github/workflows/maestro-local.yml
index b784514d13..64eff695d9 100644
--- a/.github/workflows/maestro-local.yml
+++ b/.github/workflows/maestro-local.yml
@@ -28,7 +28,7 @@ jobs:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.ref }}
- - uses: actions/setup-java@v4
+ - uses: actions/setup-java@v5
name: Use JDK 21
with:
distribution: 'temurin' # See 'Supported distributions' for available options
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index f09e36b785..b494d9fe8c 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -18,7 +18,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml
index 177b429a10..f89373b44f 100644
--- a/.github/workflows/nightlyReports.yml
+++ b/.github/workflows/nightlyReports.yml
@@ -21,7 +21,7 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -62,7 +62,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 1157a02eea..064da5b47e 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -47,7 +47,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -85,7 +85,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -125,7 +125,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -169,7 +169,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -209,7 +209,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -249,7 +249,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml
index a5172188ee..a4d54afeb4 100644
--- a/.github/workflows/recordScreenshots.yml
+++ b/.github/workflows/recordScreenshots.yml
@@ -34,7 +34,7 @@ jobs:
with:
persist-credentials: false
- name: ☕️ Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c3f51f8ec4..661110338d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -20,7 +20,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -61,7 +61,7 @@ jobs:
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -89,7 +89,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
index d85e4edf27..080fcef0e0 100644
--- a/.github/workflows/sonar.yml
+++ b/.github/workflows/sonar.yml
@@ -28,7 +28,7 @@ jobs:
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml
index 44c3a24eb7..4d1362921f 100644
--- a/.github/workflows/sync-localazy.yml
+++ b/.github/workflows/sync-localazy.yml
@@ -13,7 +13,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a0ffe74f6f..e25e22cf60 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -47,7 +47,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: ☕️ Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -82,7 +82,7 @@ jobs:
# https://github.com/codecov/codecov-action
- name: ☂️ Upload coverage reports to codecov
- uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
+ uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 03fcfb7bea..254a1fc3cc 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.maestro/tests/roomList/createAndDeleteRoom.yaml b/.maestro/tests/roomList/createAndDeleteRoom.yaml
index e957d04bdf..a72fb80075 100644
--- a/.maestro/tests/roomList/createAndDeleteRoom.yaml
+++ b/.maestro/tests/roomList/createAndDeleteRoom.yaml
@@ -3,18 +3,18 @@ appId: ${MAESTRO_APP_ID}
# Purpose: Test the creation and deletion of a room
- tapOn: "Create a new conversation or room"
- tapOn: "New room"
-- tapOn: "Search for someone"
-- inputText: ${MAESTRO_INVITEE1_MXID}
-- tapOn:
- text: ${MAESTRO_INVITEE1_MXID}
- index: 1
-- tapOn: "Next"
- tapOn: "e.g. your project name"
- inputText: "aRoomName"
- tapOn: "What is this room about?"
- inputText: "aRoomTopic"
- tapOn: "Create"
- takeScreenshot: build/maestro/320-createAndDeleteRoom
+- tapOn: "Search for someone"
+- inputText: ${MAESTRO_INVITEE1_MXID}
+- tapOn:
+ text: ${MAESTRO_INVITEE1_MXID}
+ index: 1
+- tapOn: "Finish"
- tapOn: "aRoomName"
- tapOn: "Invite"
# assert there's 1 member and 1 invitee
diff --git a/CHANGES.md b/CHANGES.md
index bed23e2dc3..3f6a82a468 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,41 @@
+Changes in Element X v25.08.3
+=============================
+
+## What's Changed
+### ✨ Features
+* Add media file limit size warning and media quality selection by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5131
+### 🐛 Bugfixes
+* Fix cursor position in room list search by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5138
+* Fix leaving the room not always dismissing the room screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5089
+* Do not automatically initialize `DefaultVideoMetadataExtractor`'s data source by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5157
+* Provide calculated server names when opening a room from another by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5155
+### 🗣 Translations
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5146
+### 🧱 Build
+* Compile and target sdk36 by @bmarty in https://github.com/element-hq/element-x-android/pull/5150
+* Fix Maestro regression when coming back from room to the search screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5156
+### Dependency upgrades
+* Update android.gradle.plugin to v8.12.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5106
+* Update wysiwyg to v2.39.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5080
+* Update dependency python to 3.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5144
+* Update rnkdsh/action-upload-diawi action to v1.5.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5141
+* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5143
+* Update actions/checkout action to v5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5148
+* Update dependency io.sentry:sentry-android to v8.19.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5149
+* Update dependency io.sentry:sentry-android to v8.19.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5158
+* Update dependency androidx.browser:browser to v1.9.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5096
+* Update Compose bom to 2025.07.00 by @bmarty in https://github.com/element-hq/element-x-android/pull/5164
+* Update showkase to v1.0.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5117
+* Update haze to v1.6.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5167
+### Others
+* Let enterprise build be able to override (or disable) the bug report URL. by @bmarty in https://github.com/element-hq/element-x-android/pull/5139
+* Hide the recovery key while we are entering it by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5147
+* Remove old feature flags by @bmarty in https://github.com/element-hq/element-x-android/pull/5160
+* Move push history entry point from notification settings to developer settings by @bmarty in https://github.com/element-hq/element-x-android/pull/5161
+
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.08.2...v25.08.3
+
Changes in Element X v25.08.2
=============================
diff --git a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt b/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt
index 2511e0008d..7b41c03158 100644
--- a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt
+++ b/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt
@@ -28,6 +28,7 @@ import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeSpec
+import com.squareup.kotlinpoet.ksp.addOriginatingKSFile
import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo
import dagger.Binds
@@ -78,6 +79,7 @@ class ContributesNodeProcessor(
)
.addType(
TypeSpec.classBuilder(moduleClassName)
+ .addOriginatingKSFile(ksClass.containingFile!!)
.addModifiers(KModifier.ABSTRACT)
.addAnnotation(Module::class)
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.toTypeName()).build())
@@ -102,10 +104,7 @@ class ContributesNodeProcessor(
content.writeTo(
codeGenerator = codeGenerator,
- dependencies = Dependencies(
- aggregating = true,
- ksClass.containingFile!!
- ),
+ dependencies = Dependencies(aggregating = false),
)
}
@@ -139,6 +138,7 @@ class ContributesNodeProcessor(
val content = FileSpec.builder(generatedPackage, assistedFactoryClassName)
.addType(
TypeSpec.interfaceBuilder(assistedFactoryClassName)
+ .addOriginatingKSFile(ksClass.containingFile!!)
.addSuperinterface(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(nodeClassName))
.addAnnotation(AssistedFactory::class)
.addFunction(
@@ -155,10 +155,7 @@ class ContributesNodeProcessor(
content.writeTo(
codeGenerator = codeGenerator,
- dependencies = Dependencies(
- aggregating = true,
- ksClass.containingFile!!
- ),
+ dependencies = Dependencies(aggregating = false),
)
}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ea45e51bf8..6131abf385 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -301,6 +301,7 @@ dependencies {
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.toolbox.test)
+ testImplementation(projects.tests.testutils)
koverDependencies()
}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 89dcab5fad..c55b1bf1c2 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -42,3 +42,12 @@
}
-keep class io.element.android.x.di.** { *; }
+
+
+# Keep LogSessionId class and related classes (https://github.com/androidx/media/issues/2535)
+-keep class android.media.metrics.LogSessionId { *; }
+-keep class android.media.metrics.** { *; }
+
+# Keep Media3 classes that use reflection (https://github.com/androidx/media/issues/2535)
+-keep class androidx.media3.** { *; }
+-dontwarn android.media.metrics.**
diff --git a/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt
index 72e864bbea..d2fbb1b78f 100644
--- a/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt
+++ b/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt
@@ -10,10 +10,14 @@ package io.element.android.x.initializer
import android.content.Context
import androidx.startup.Initializer
import io.element.android.features.rageshake.impl.crash.VectorUncaughtExceptionHandler
+import io.element.android.features.rageshake.impl.di.RageshakeBindings
+import io.element.android.libraries.architecture.bindings
class CrashInitializer : Initializer {
override fun create(context: Context) {
- VectorUncaughtExceptionHandler(context).activate()
+ VectorUncaughtExceptionHandler(
+ context.bindings().preferencesCrashDataStore(),
+ ).activate()
}
override fun dependencies(): List>> = emptyList()
diff --git a/app/src/main/kotlin/io/element/android/x/intent/DefaultIntentProvider.kt b/app/src/main/kotlin/io/element/android/x/intent/DefaultIntentProvider.kt
index fedcdf2919..cce6d76510 100644
--- a/app/src/main/kotlin/io/element/android/x/intent/DefaultIntentProvider.kt
+++ b/app/src/main/kotlin/io/element/android/x/intent/DefaultIntentProvider.kt
@@ -11,7 +11,7 @@ import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.libraries.deeplink.DeepLinkCreator
+import io.element.android.libraries.deeplink.api.DeepLinkCreator
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.core.RoomId
@@ -33,7 +33,7 @@ class DefaultIntentProvider @Inject constructor(
): Intent {
return Intent(context, MainActivity::class.java).apply {
action = Intent.ACTION_VIEW
- data = deepLinkCreator.room(sessionId, roomId, threadId).toUri()
+ data = deepLinkCreator.create(sessionId, roomId, threadId).toUri()
}
}
}
diff --git a/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt b/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt
index 5e81d6b964..9d6d9d4320 100644
--- a/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt
+++ b/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt
@@ -5,15 +5,22 @@
* Please see LICENSE files in the repository root for full details.
*/
+@file:Suppress("SameParameterValue")
+
package io.element.android.x.intent
import android.content.Context
import android.content.Intent
import com.google.common.truth.Truth.assertThat
-import io.element.android.libraries.deeplink.DeepLinkCreator
+import io.element.android.libraries.deeplink.api.DeepLinkCreator
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
import io.element.android.x.MainActivity
import org.junit.Test
import org.junit.runner.RunWith
@@ -23,45 +30,31 @@ import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class DefaultIntentProviderTest {
@Test
- fun `test getViewRoomIntent with Session`() {
- val sut = createDefaultIntentProvider()
- val result = sut.getViewRoomIntent(
- sessionId = A_SESSION_ID,
- roomId = null,
- threadId = null,
+ fun `test getViewRoomIntent with data`() {
+ val deepLinkCreator = lambdaRecorder { _, _, _ -> "deepLinkCreatorResult" }
+ val sut = createDefaultIntentProvider(
+ deepLinkCreator = { sessionId, roomId, threadId -> deepLinkCreator.invoke(sessionId, roomId, threadId) },
)
- result.commonAssertions()
- assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org")
- }
-
- @Test
- fun `test getViewRoomIntent with Session and Room`() {
- val sut = createDefaultIntentProvider()
- val result = sut.getViewRoomIntent(
- sessionId = A_SESSION_ID,
- roomId = A_ROOM_ID,
- threadId = null,
- )
- result.commonAssertions()
- assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain")
- }
-
- @Test
- fun `test getViewRoomIntent with Session, Room and Thread`() {
- val sut = createDefaultIntentProvider()
val result = sut.getViewRoomIntent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = A_THREAD_ID,
)
result.commonAssertions()
- assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
+ assertThat(result.data.toString()).isEqualTo("deepLinkCreatorResult")
+ deepLinkCreator.assertions().isCalledOnce().with(
+ value(A_SESSION_ID),
+ value(A_ROOM_ID),
+ value(A_THREAD_ID),
+ )
}
- private fun createDefaultIntentProvider(): DefaultIntentProvider {
+ private fun createDefaultIntentProvider(
+ deepLinkCreator: DeepLinkCreator = DeepLinkCreator { _, _, _ -> "" },
+ ): DefaultIntentProvider {
return DefaultIntentProvider(
context = RuntimeEnvironment.getApplication() as Context,
- deepLinkCreator = DeepLinkCreator(),
+ deepLinkCreator = deepLinkCreator,
)
}
diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts
index e6b50eaaf6..715dd79d9b 100644
--- a/appnav/build.gradle.kts
+++ b/appnav/build.gradle.kts
@@ -27,7 +27,7 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
- implementation(projects.libraries.deeplink)
+ implementation(projects.libraries.deeplink.api)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.oidc.api)
implementation(projects.libraries.preferences.api)
@@ -65,6 +65,7 @@ dependencies {
testImplementation(projects.features.rageshake.test)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.analytics.test)
+ testImplementation(projects.services.toolbox.test)
testImplementation(libs.test.appyx.junit)
testImplementation(libs.test.arch.core)
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
index fb9b38352c..f621c7a090 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
@@ -46,7 +46,6 @@ import io.element.android.appnav.loggedin.SendQueues
import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
-import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.api.state.FtueService
@@ -60,6 +59,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.share.api.ShareEntryPoint
+import io.element.android.features.startchat.api.StartChatEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
import io.element.android.libraries.architecture.BackstackView
@@ -81,6 +81,7 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.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.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
@@ -94,15 +95,6 @@ import java.time.Duration
import java.time.Instant
import java.util.Optional
import java.util.UUID
-import kotlin.collections.List
-import kotlin.collections.any
-import kotlin.collections.emptyList
-import kotlin.collections.first
-import kotlin.collections.forEach
-import kotlin.collections.listOf
-import kotlin.collections.mapNotNull
-import kotlin.collections.plus
-import kotlin.collections.setOf
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toKotlinDuration
@@ -113,7 +105,7 @@ class LoggedInFlowNode @AssistedInject constructor(
@Assisted plugins: List,
private val homeEntryPoint: HomeEntryPoint,
private val preferencesEntryPoint: PreferencesEntryPoint,
- private val createRoomEntryPoint: CreateRoomEntryPoint,
+ private val startChatEntryPoint: StartChatEntryPoint,
private val appNavigationStateService: AppNavigationStateService,
private val secureBackupEntryPoint: SecureBackupEntryPoint,
private val userProfileEntryPoint: UserProfileEntryPoint,
@@ -130,6 +122,7 @@ class LoggedInFlowNode @AssistedInject constructor(
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
private val sessionEnterpriseService: SessionEnterpriseService,
private val networkMonitor: NetworkMonitor,
+ private val notificationConversationService: NotificationConversationService,
snackbarDispatcher: SnackbarDispatcher,
) : BaseFlowNode(
backstack = BackStack(
@@ -215,6 +208,12 @@ class LoggedInFlowNode @AssistedInject constructor(
}
.launchIn(lifecycleScope)
},
+ onResume = {
+ lifecycleScope.launch {
+ val availableRoomIds = matrixClient.getJoinedRoomIds().getOrNull() ?: return@launch
+ notificationConversationService.onAvailableRoomsChanged(sessionId = matrixClient.sessionId, roomIds = availableRoomIds)
+ }
+ },
onDestroy = {
appNavigationStateService.onLeavingSpace(id)
appNavigationStateService.onLeavingSession(id)
@@ -304,7 +303,7 @@ class LoggedInFlowNode @AssistedInject constructor(
backstack.push(NavTarget.Settings())
}
- override fun onCreateRoomClick() {
+ override fun onStartChatClick() {
backstack.push(NavTarget.CreateRoom)
}
@@ -422,7 +421,7 @@ class LoggedInFlowNode @AssistedInject constructor(
.build()
}
NavTarget.CreateRoom -> {
- val callback = object : CreateRoomEntryPoint.Callback {
+ val callback = object : StartChatEntryPoint.Callback {
override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List) {
backstack.replace(NavTarget.Room(roomIdOrAlias = roomIdOrAlias, serverNames = serverNames))
}
@@ -432,7 +431,7 @@ class LoggedInFlowNode @AssistedInject constructor(
}
}
- createRoomEntryPoint
+ startChatEntryPoint
.nodeBuilder(this, buildContext)
.callback(callback)
.build()
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
index 3d02739ba6..c64288839b 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
@@ -44,7 +44,7 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.waitForChildAttached
import io.element.android.libraries.core.uri.ensureProtocol
-import io.element.android.libraries.deeplink.DeeplinkData
+import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
index 786b694c3f..0c8a7cb17f 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
@@ -10,8 +10,8 @@ package io.element.android.appnav.intent
import android.content.Intent
import io.element.android.features.login.api.LoginIntentResolver
import io.element.android.features.login.api.LoginParams
-import io.element.android.libraries.deeplink.DeeplinkData
-import io.element.android.libraries.deeplink.DeeplinkParser
+import io.element.android.libraries.deeplink.api.DeeplinkData
+import io.element.android.libraries.deeplink.api.DeeplinkParser
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.oidc.api.OidcAction
diff --git a/appnav/src/main/res/values-ur/translations.xml b/appnav/src/main/res/values-ur/translations.xml
new file mode 100644
index 0000000000..80d1840d9d
--- /dev/null
+++ b/appnav/src/main/res/values-ur/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "لاگ آؤٹ اور اپ گریڈ کریں"
+ "آپ کا homeserver اب پرانے پروٹوکول کو سپورٹ نہیں کرتا ہے۔ براہ کرم لاگ آؤٹ کریں اور ایپ کا استعمال جاری رکھنے کے لیے دوبارہ لاگ ان کریں۔"
+
diff --git a/appnav/src/main/res/values-uz/translations.xml b/appnav/src/main/res/values-uz/translations.xml
new file mode 100644
index 0000000000..a76aaaed92
--- /dev/null
+++ b/appnav/src/main/res/values-uz/translations.xml
@@ -0,0 +1,6 @@
+
+
+ "Tizmdan chiqish va yangilash"
+ "%1$s endi eski protokolni qoʻllab-quvvatlamaydi. Iltimos, ilovadan foydalanishni davom ettirish uchun tizimdan chiqing va qayta kiring."
+ "Sizning uy serveringiz endi eski protokolni qoʻllab-quvvatlamaydi. Iltimos, ilovadan foydalanishni davom ettirish uchun tizimdan qayta chiqib-kiring."
+
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt
index 4c8b2a803d..2a343a1592 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt
@@ -19,6 +19,7 @@ import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.apperror.api.AppErrorState
import io.element.android.services.apperror.api.AppErrorStateService
import io.element.android.services.apperror.impl.DefaultAppErrorStateService
+import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@@ -42,7 +43,9 @@ class RootPresenterTest {
@Test
fun `present - passes app error state`() = runTest {
val presenter = createRootPresenter(
- appErrorService = DefaultAppErrorStateService().apply {
+ appErrorService = DefaultAppErrorStateService(
+ stringProvider = FakeStringProvider(),
+ ).apply {
showError("Bad news", "Something bad happened")
}
)
@@ -61,7 +64,9 @@ class RootPresenterTest {
}
private fun createRootPresenter(
- appErrorService: AppErrorStateService = DefaultAppErrorStateService(),
+ appErrorService: AppErrorStateService = DefaultAppErrorStateService(
+ stringProvider = FakeStringProvider(),
+ ),
): RootPresenter {
return RootPresenter(
crashDetectionPresenter = { aCrashDetectionState() },
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt
index e95eb66cc3..05898c75f3 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt
@@ -14,9 +14,7 @@ import androidx.core.net.toUri
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.test.FakeLoginIntentResolver
-import io.element.android.libraries.deeplink.DeepLinkCreator
-import io.element.android.libraries.deeplink.DeeplinkData
-import io.element.android.libraries.deeplink.DeeplinkParser
+import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.test.A_ROOM_ID
@@ -46,15 +44,11 @@ class IntentResolverTest {
@Test
fun `test resolve navigation intent root`() {
- val sut = createIntentResolver()
+ val sut = createIntentResolver(
+ deeplinkParserResult = DeeplinkData.Root(A_SESSION_ID)
+ )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
- data = DeepLinkCreator().room(
- sessionId = A_SESSION_ID,
- roomId = null,
- threadId = null,
- )
- .toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
@@ -68,15 +62,15 @@ class IntentResolverTest {
@Test
fun `test resolve navigation intent room`() {
- val sut = createIntentResolver()
- val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
- action = Intent.ACTION_VIEW
- data = DeepLinkCreator().room(
+ val sut = createIntentResolver(
+ deeplinkParserResult = DeeplinkData.Room(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = null,
)
- .toUri()
+ )
+ val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
+ action = Intent.ACTION_VIEW
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
@@ -92,15 +86,15 @@ class IntentResolverTest {
@Test
fun `test resolve navigation intent thread`() {
- val sut = createIntentResolver()
- val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
- action = Intent.ACTION_VIEW
- data = DeepLinkCreator().room(
+ val sut = createIntentResolver(
+ deeplinkParserResult = DeeplinkData.Room(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = A_THREAD_ID,
)
- .toUri()
+ )
+ val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
+ action = Intent.ACTION_VIEW
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
@@ -240,12 +234,13 @@ class IntentResolverTest {
}
private fun createIntentResolver(
+ deeplinkParserResult: DeeplinkData? = null,
permalinkParserResult: (String) -> PermalinkData = { lambdaError() },
loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() },
oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() },
): IntentResolver {
return IntentResolver(
- deeplinkParser = DeeplinkParser(),
+ deeplinkParser = { deeplinkParserResult },
loginIntentResolver = FakeLoginIntentResolver(
parseResult = loginIntentResolverResult,
),
diff --git a/fastlane/metadata/android/en-US/changelogs/202508040.txt b/fastlane/metadata/android/en-US/changelogs/202508040.txt
new file mode 100644
index 0000000000..ac9a4fb45b
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202508040.txt
@@ -0,0 +1,2 @@
+Main changes in this version: you can now create shortcuts to your recent conversations, several bug fixes related to media processing and downloading.
+Full changelog: https://github.com/element-hq/element-x-android/releases
\ No newline at end of file
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt
index e6917c237e..ec37542b31 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt
@@ -8,9 +8,8 @@
package io.element.android.features.analytics.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import javax.inject.Inject
-open class AnalyticsOptInStateProvider @Inject constructor() : PreviewParameterProvider {
+open class AnalyticsOptInStateProvider : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aAnalyticsOptInState(),
diff --git a/features/call/impl/src/main/res/values-uz/translations.xml b/features/call/impl/src/main/res/values-uz/translations.xml
index 010695a2ef..daab8211cb 100644
--- a/features/call/impl/src/main/res/values-uz/translations.xml
+++ b/features/call/impl/src/main/res/values-uz/translations.xml
@@ -2,5 +2,6 @@
"Davom etayotgan qo\'ng\'iroq"
"Qo\'ng\'iroqqa qaytish uchun bosing"
- "☎️ Qo‘ng‘iroq davom etmoqda"
+ "☎️ Qoʻngʻiroq davom etmoqda"
+ "Element Call ushbu Android versiyasida Bluetooth audio qurilmalaridan foydalanishni qoʻllab-quvvatlamaydi. Iltimos, boshqa audio qurilmani tanlang."
diff --git a/features/call/impl/src/main/res/values-zh-rTW/translations.xml b/features/call/impl/src/main/res/values-zh-rTW/translations.xml
index ec01e5a6e3..d0e400dd07 100644
--- a/features/call/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/call/impl/src/main/res/values-zh-rTW/translations.xml
@@ -3,5 +3,6 @@
"進行中的通話"
"點擊以返回到通話頁面"
"☎️ 通話中"
+ "Element Call 不支援在此 Android 版本中使用藍牙音訊裝置。請選取其他音訊裝置。"
"Element 來電"
diff --git a/features/call/impl/src/main/res/values-zh/translations.xml b/features/call/impl/src/main/res/values-zh/translations.xml
index 8eda0df212..6192568a61 100644
--- a/features/call/impl/src/main/res/values-zh/translations.xml
+++ b/features/call/impl/src/main/res/values-zh/translations.xml
@@ -3,5 +3,6 @@
"通话进行中"
"点按即可返回通话"
"☎️ 通话中"
+ "Element Call 不支持在此 Android 版本中使用蓝牙音频设备。请选择其他音频设备。"
"Element 来电"
diff --git a/features/changeroommemberroles/api/build.gradle.kts b/features/changeroommemberroles/api/build.gradle.kts
index 9ec13286da..c58e25cf1c 100644
--- a/features/changeroommemberroles/api/build.gradle.kts
+++ b/features/changeroommemberroles/api/build.gradle.kts
@@ -1,5 +1,3 @@
-import extension.setupAnvil
-
/*
* Copyright 2025 New Vector Ltd.
*
@@ -16,8 +14,6 @@ android {
namespace = "io.element.android.features.changeroommemberroles.api"
}
-setupAnvil()
-
dependencies {
implementation(projects.anvilannotations)
implementation(projects.libraries.architecture)
diff --git a/features/changeroommemberroles/impl/src/main/res/values-et/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-et/translations.xml
index 044503f1de..a43a9a89d0 100644
--- a/features/changeroommemberroles/impl/src/main/res/values-et/translations.xml
+++ b/features/changeroommemberroles/impl/src/main/res/values-et/translations.xml
@@ -2,7 +2,7 @@
"Vaid peakasutajad"
"Suhtluskeelu seadmine"
- "Sõnumite kustutamine"
+ "Eemalda sõnumid"
"Kõik"
"Kutsu teisi osalejaid ja vasta ise liitumiskutsetele"
"Jututoas osalejate modereerimine"
diff --git a/features/changeroommemberroles/impl/src/main/res/values-eu/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-eu/translations.xml
index a6b701f0a0..47210a0ca6 100644
--- a/features/changeroommemberroles/impl/src/main/res/values-eu/translations.xml
+++ b/features/changeroommemberroles/impl/src/main/res/values-eu/translations.xml
@@ -16,6 +16,7 @@
"Bidali mezuak"
"Editatu administratzaileak"
"Administratzailea gehitu?"
+ "Jabetza eskualdatu?"
"Jaitsi mailaz"
"Ezin izango duzu hau aldatu zure burua mailaz jaisten ari zarelako, zu bazara gelan baimenak dituen azken erabiltzailea ezin izango dira baimenak berreskuratu."
"Zure burua mailaz jaitsi?"
@@ -23,6 +24,7 @@
"(Egiteke)"
"Administratzaileek automatikoki dute moderatzaile-pribilegioak"
"Editatu moderatzaileak"
+ "Aukeratu jabeak"
"Administratzaileak"
"Moderatzaileak"
"Kideak"
@@ -42,15 +44,18 @@
"Zain"
"Kudeatzailea"
"Moderatzailea"
+ "Jabea"
"Gelako kideak"
"%1$s(r)i debekua kentzen"
"Administratzaileak"
+ "Administratzaileak eta jabeak"
"Aldatu nire rola"
"Jaitsi maila, kidera"
"Jaitsi maila, moderatzailera"
"Kideen moderazioa"
"Mezuak eta edukiak"
"Moderatzaileak"
+ "Jabeak"
"Baimenak"
"Berrezarri baimenak"
"Baimenak berrezarritakoan, uneko ezarpenak galduko dituzu."
diff --git a/features/changeroommemberroles/impl/src/main/res/values-fa/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-fa/translations.xml
index c9c9da0882..c5bbc9d3ff 100644
--- a/features/changeroommemberroles/impl/src/main/res/values-fa/translations.xml
+++ b/features/changeroommemberroles/impl/src/main/res/values-fa/translations.xml
@@ -29,6 +29,10 @@
"اعضا"
"تغییراتی ذخیره نشده دارید."
"ذخیرهٔ تغییرات؟"
+
+ - "%1$d نفر"
+ - "%1$d نفر"
+
"برداشت و تحریم عضو"
"تنها برداشتن عضو"
"رفع انسداد"
diff --git a/features/changeroommemberroles/impl/src/main/res/values-in/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-in/translations.xml
index 1f25e9ada3..3bc68c154e 100644
--- a/features/changeroommemberroles/impl/src/main/res/values-in/translations.xml
+++ b/features/changeroommemberroles/impl/src/main/res/values-in/translations.xml
@@ -2,7 +2,7 @@
"Hanya admin"
"Cekal orang-orang"
- "Hapus pesan"
+ "Hilangkan pesan"
"Semua orang"
"Undang orang-orang dan terima permintaan untuk bergabung"
"Moderasi anggota"
diff --git a/features/changeroommemberroles/impl/src/main/res/values-sv/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-sv/translations.xml
index 9153ccc89f..a94b7384fa 100644
--- a/features/changeroommemberroles/impl/src/main/res/values-sv/translations.xml
+++ b/features/changeroommemberroles/impl/src/main/res/values-sv/translations.xml
@@ -17,13 +17,17 @@
"Redigera administratörer"
"Du kommer inte att kunna ångra den här åtgärden. Du befordrar användaren till att ha samma behörighetsnivå som du."
"Lägg till Admin?"
+ "Du kommer inte att kunna ångra den här åtgärden. Du överför ägarskapet till de valda användarna. När du lämnar kommer detta att vara permanent."
+ "Överför ägarskap?"
"Degradera"
"Du kommer inte att kunna ångra denna ändring eftersom du degraderar dig själv, om du är den sista privilegierade användaren i rummet kommer det att vara omöjligt att återfå privilegier."
"Degradera dig själv?"
"%1$s (Väntar)"
"(Väntar)"
"Administratörer har automatiskt moderatorbehörighet"
+ "Ägare har automatiskt administratörsbehörighet."
"Redigera moderatorer"
+ "Välj ägare"
"Administratörer"
"Moderatorer"
"Medlemmar"
@@ -44,15 +48,18 @@
"Väntar"
"Admin"
"Moderator"
+ "Ägare"
"Rumsmedlemmar"
"Avbannar %1$s"
"Administratörer"
+ "Administratörer och ägare"
"Ändra min roll"
"Degradera till medlem"
"Degradera till moderator"
"Medlemsmoderering"
"Meddelanden och innehåll"
"Moderatorer"
+ "Ägare"
"Behörigheter"
"Återställ behörigheter"
"När du har återställt behörigheterna kommer du att förlora de aktuella inställningarna."
diff --git a/features/changeroommemberroles/impl/src/main/res/values-zh-rTW/translations.xml b/features/changeroommemberroles/impl/src/main/res/values-zh-rTW/translations.xml
index 0aaee24ce2..2ec5ec64c5 100644
--- a/features/changeroommemberroles/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/changeroommemberroles/impl/src/main/res/values-zh-rTW/translations.xml
@@ -17,13 +17,17 @@
"編輯管理員"
"您將無法復原此動作。您正將使用者提昇至與您相同的權力等級。"
"要新增管理員嗎?"
+ "您將無法撤銷此動作。您正在將所有權轉移給選定的使用者。一旦您離開,此動作將永久有效。"
+ "轉移所有權?"
"降級"
"當您自行降級時,您將無法復原此變更,若您是聊天室中的最後一位特權使用者,則無法重新獲得權限。"
"將自己降級?"
"%1$s(擱置中)"
"(擱置中)"
"管理員自動擁有版主權限"
+ "擁有者自動擁有管理員權限。"
"編輯版主"
+ "選擇擁有者"
"管理員"
"版主"
"成員"
@@ -43,15 +47,18 @@
"待定"
"管理員"
"版主"
+ "擁有者"
"聊天室成員"
"正在解除黑名單 %1$s"
"管理員"
+ "管理員與擁有者"
"變更我的身份"
"降級為普通成員"
"降級為版主"
"成員管理"
"訊息與內容"
"版主"
+ "擁有者"
"權限"
"重設權限"
"重設之後,您會遺失當前的設定。"
diff --git a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt
index 3b35d0a3e1..5fc71f2a3b 100644
--- a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt
+++ b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
@@ -11,17 +11,17 @@ 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.RoomIdOrAlias
+import io.element.android.libraries.matrix.api.core.RoomId
interface CreateRoomEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
+
interface NodeBuilder {
fun callback(callback: Callback): NodeBuilder
fun build(): Node
}
interface Callback : Plugin {
- fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List)
- fun onOpenRoomDirectory()
+ fun onRoomCreated(roomId: RoomId)
}
}
diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts
index 769f7b5628..d2306837d0 100644
--- a/features/createroom/impl/build.gradle.kts
+++ b/features/createroom/impl/build.gradle.kts
@@ -1,4 +1,3 @@
-import extension.ComponentMergingStrategy
import extension.setupAnvil
/*
@@ -23,7 +22,7 @@ android {
}
}
-setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
+setupAnvil()
dependencies {
implementation(projects.libraries.core)
@@ -33,7 +32,7 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.androidutils)
- implementation(projects.libraries.deeplink)
+ implementation(projects.libraries.deeplink.api)
implementation(projects.libraries.mediapickers.api)
implementation(projects.libraries.mediaupload.api)
implementation(projects.libraries.permissions.api)
@@ -41,6 +40,7 @@ dependencies {
implementation(projects.services.analytics.api)
implementation(libs.coil.compose)
implementation(projects.libraries.featureflag.api)
+ implementation(projects.features.invitepeople.api)
api(projects.features.createroom.api)
testImplementation(libs.test.junit)
@@ -56,7 +56,7 @@ dependencies {
testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.usersearch.test)
- testImplementation(projects.features.createroom.test)
+ testImplementation(projects.features.startchat.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit)
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt
deleted file mode 100644
index a7678f130f..0000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl
-
-import android.os.Parcelable
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import com.bumble.appyx.core.modality.BuildContext
-import com.bumble.appyx.core.node.Node
-import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
-import com.bumble.appyx.navmodel.backstack.BackStack
-import com.bumble.appyx.navmodel.backstack.operation.push
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.createroom.CreateRoomNavigator
-import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
-import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
-import io.element.android.features.createroom.impl.di.CreateRoomComponent
-import io.element.android.libraries.architecture.BackstackView
-import io.element.android.libraries.architecture.BaseFlowNode
-import io.element.android.libraries.architecture.bindings
-import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.DaggerComponentOwner
-import io.element.android.libraries.di.SessionScope
-import kotlinx.parcelize.Parcelize
-
-@ContributesNode(SessionScope::class)
-class ConfigureRoomFlowNode @AssistedInject constructor(
- @Assisted buildContext: BuildContext,
- @Assisted plugins: List,
-) : DaggerComponentOwner,
- BaseFlowNode(
- backstack = BackStack(
- initialElement = NavTarget.Root,
- savedStateMap = buildContext.savedStateMap,
- ),
- buildContext = buildContext,
- plugins = plugins
- ) {
- private val component by lazy {
- parent!!.bindings().createRoomComponentBuilder().build()
- }
- private val navigator = plugins().first()
-
- override val daggerComponent: Any
- get() = component
-
- sealed interface NavTarget : Parcelable {
- @Parcelize
- data object Root : NavTarget
-
- @Parcelize
- data object ConfigureRoom : NavTarget
- }
-
- override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
- return when (navTarget) {
- NavTarget.Root -> {
- val callback = object : AddPeopleNode.Callback {
- override fun onContinue() {
- backstack.push(NavTarget.ConfigureRoom)
- }
- }
- createNode(buildContext = buildContext, plugins = listOf(callback))
- }
- NavTarget.ConfigureRoom -> {
- createNode(buildContext = buildContext, plugins = listOf(navigator))
- }
- }
- }
-
- @Composable
- override fun View(modifier: Modifier) {
- BackstackView()
- }
-}
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 b8b755a0b3..6c819edf5a 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
@@ -8,28 +8,25 @@
package io.element.android.features.createroom.impl
import android.os.Parcelable
-import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
-import com.bumble.appyx.core.navigation.transition.JumpToEndTransitionHandler
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
+import com.bumble.appyx.navmodel.backstack.operation.replace
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.createroom.DefaultCreateRoomNavigator
import io.element.android.features.createroom.api.CreateRoomEntryPoint
-import io.element.android.features.createroom.impl.joinbyaddress.JoinRoomByAddressNode
-import io.element.android.features.createroom.impl.root.CreateRoomRootNode
+import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
+import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
-import io.element.android.libraries.architecture.OverlayView
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@@ -38,53 +35,48 @@ class CreateRoomFlowNode @AssistedInject constructor(
@Assisted plugins: List,
) : BaseFlowNode(
backstack = BackStack(
- initialElement = NavTarget.Root,
+ initialElement = NavTarget.ConfigureRoom,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins
) {
- sealed interface NavTarget : Parcelable {
- @Parcelize
- data object Root : NavTarget
-
- @Parcelize
- data object NewRoom : NavTarget
-
- @Parcelize
- data object JoinByAddress : NavTarget
+ private fun onRoomCreated(roomId: RoomId) {
+ plugins().forEach { it.onRoomCreated(roomId) }
}
- private val navigator = DefaultCreateRoomNavigator(
- backstack = backstack,
- overlay = overlay,
- openRoom = { roomIdOrAlias, viaServers ->
- plugins().forEach { it.onOpenRoom(roomIdOrAlias, viaServers) }
- },
- openRoomDirectory = {
- plugins().forEach { it.onOpenRoomDirectory() }
- }
- )
-
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
- NavTarget.Root -> {
- createNode(buildContext = buildContext, plugins = listOf(navigator))
+ NavTarget.ConfigureRoom -> {
+ val callback = object : ConfigureRoomNode.Callback {
+ override fun onCreateRoomSuccess(roomId: RoomId) {
+ backstack.replace(NavTarget.AddPeople(roomId))
+ }
+ }
+ createNode(buildContext, plugins = listOf(callback))
}
- NavTarget.NewRoom -> {
- createNode(buildContext = buildContext, plugins = listOf(navigator))
- }
- NavTarget.JoinByAddress -> {
- createNode(buildContext = buildContext, plugins = listOf(navigator))
+ is NavTarget.AddPeople -> {
+ val inputs = AddPeopleNode.Inputs(navTarget.roomId)
+ val callback: AddPeopleNode.Callback = object : AddPeopleNode.Callback {
+ override fun onFinish() {
+ onRoomCreated(navTarget.roomId)
+ }
+ }
+ createNode(buildContext, plugins = listOf(inputs, callback))
}
}
}
@Composable
override fun View(modifier: Modifier) {
- Box(modifier = modifier) {
- BackstackView()
- OverlayView(transitionHandler = remember { JumpToEndTransitionHandler() })
- }
+ BackstackView()
+ }
+
+ sealed interface NavTarget : Parcelable {
+ @Parcelize
+ data object ConfigureRoom : NavTarget
+
+ @Parcelize
+ data class AddPeople(val roomId: RoomId) : NavTarget
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt
index 161d67e817..b9dfb20960 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
@@ -13,10 +13,10 @@ import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.SessionScope
import javax.inject.Inject
-@ContributesBinding(AppScope::class)
+@ContributesBinding(SessionScope::class)
class DefaultCreateRoomEntryPoint @Inject constructor() : CreateRoomEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreateRoomEntryPoint.NodeBuilder {
val plugins = ArrayList()
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt
index ba9eb82073..3fc9cd7d81 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
@@ -16,30 +16,46 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.createroom.impl.di.CreateRoomScope
+import io.element.android.features.invitepeople.api.InvitePeoplePresenter
+import io.element.android.features.invitepeople.api.InvitePeopleRenderer
+import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.inputs
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.core.RoomId
-@ContributesNode(CreateRoomScope::class)
+@ContributesNode(SessionScope::class)
class AddPeopleNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- private val presenter: AddPeoplePresenter,
+ invitePeoplePresenterFactory: InvitePeoplePresenter.Factory,
+ private val invitePeopleRenderer: InvitePeopleRenderer,
) : Node(buildContext, plugins = plugins) {
+ data class Inputs(
+ val roomId: RoomId,
+ ) : NodeInputs
+
interface Callback : Plugin {
- fun onContinue()
+ fun onFinish()
}
- private fun onContinue() {
- plugins().forEach { it.onContinue() }
+ private fun onFinish() {
+ plugins().forEach { it.onFinish() }
}
+ private val roomId = inputs().roomId
+ private val invitePeoplePresenter = invitePeoplePresenterFactory.create(
+ joinedRoom = null,
+ roomId = roomId,
+ )
+
@Composable
override fun View(modifier: Modifier) {
- val state = presenter.present()
+ val state = invitePeoplePresenter.present()
AddPeopleView(
state = state,
- modifier = modifier,
- onBackClick = this::navigateUp,
- onNextClick = this::onContinue,
- )
+ onFinish = ::onFinish,
+ ) {
+ invitePeopleRenderer.Render(state, Modifier)
+ }
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt
deleted file mode 100644
index e050a0738e..0000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.addpeople
-
-import androidx.compose.runtime.Composable
-import io.element.android.features.createroom.impl.CreateRoomDataStore
-import io.element.android.features.createroom.impl.userlist.SelectionMode
-import io.element.android.features.createroom.impl.userlist.UserListPresenter
-import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
-import io.element.android.features.createroom.impl.userlist.UserListState
-import io.element.android.libraries.architecture.Presenter
-import io.element.android.libraries.usersearch.api.UserRepository
-import javax.inject.Inject
-
-class AddPeoplePresenter @Inject constructor(
- userListPresenterFactory: UserListPresenter.Factory,
- userRepository: UserRepository,
- dataStore: CreateRoomDataStore,
-) : Presenter {
- private val userListPresenter = userListPresenterFactory.create(
- UserListPresenterArgs(
- selectionMode = SelectionMode.Multiple,
- ),
- userRepository,
- dataStore.selectedUserListDataStore,
- )
-
- @Composable
- override fun present(): UserListState {
- return userListPresenter.present()
- }
-}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt
deleted file mode 100644
index 9e0ddd04c8..0000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.addpeople
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import io.element.android.features.createroom.impl.userlist.SelectionMode
-import io.element.android.features.createroom.impl.userlist.UserListState
-import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList
-import io.element.android.features.createroom.impl.userlist.aUserListState
-import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
-import io.element.android.libraries.matrix.ui.components.aMatrixUserList
-import io.element.android.libraries.usersearch.api.UserSearchResult
-import kotlinx.collections.immutable.toImmutableList
-
-open class AddPeopleUserListStateProvider : PreviewParameterProvider {
- override val values: Sequence
- get() = sequenceOf(
- aUserListState(),
- aUserListState(
- searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
- selectedUsers = aMatrixUserList().toImmutableList(),
- isSearchActive = false,
- selectionMode = SelectionMode.Multiple,
- ),
- aUserListState(
- searchResults = SearchBarResultState.Results(
- aMatrixUserList()
- .mapIndexed { index, matrixUser ->
- UserSearchResult(matrixUser, index % 2 == 0)
- }
- .toImmutableList()
- ),
- selectedUsers = aMatrixUserList().toImmutableList(),
- isSearchActive = true,
- selectionMode = SelectionMode.Multiple,
- ),
- aUserListState(
- recentDirectRooms = aRecentDirectRoomList(),
- ),
- )
-}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt
index d39ac4d250..1c9a5f84d3 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
@@ -7,77 +7,68 @@
package io.element.android.features.createroom.impl.addpeople
-import androidx.compose.foundation.layout.consumeWindowInsets
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
import io.element.android.features.createroom.impl.R
-import io.element.android.features.createroom.impl.components.UserListView
-import io.element.android.features.createroom.impl.userlist.UserListEvents
-import io.element.android.features.createroom.impl.userlist.UserListState
-import io.element.android.libraries.designsystem.components.button.BackButton
+import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.features.invitepeople.api.InvitePeopleState
+import io.element.android.features.invitepeople.api.InvitePeopleStateProvider
+import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import io.element.android.libraries.designsystem.theme.components.Scaffold
+import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun AddPeopleView(
- state: UserListState,
- onBackClick: () -> Unit,
- onNextClick: () -> Unit,
+ state: InvitePeopleState,
+ onFinish: () -> Unit,
modifier: Modifier = Modifier,
+ invitePeopleView: @Composable () -> Unit,
) {
- Scaffold(
+ HeaderFooterPage(
modifier = modifier,
+ contentPadding = PaddingValues(0.dp),
topBar = {
- AddPeopleViewTopBar(
- hasSelectedUsers = state.selectedUsers.isNotEmpty(),
- onBackClick = {
- if (state.isSearchActive) {
- state.eventSink(UserListEvents.OnSearchActiveChanged(false))
- } else {
- onBackClick()
- }
+ AddPeopleTopBar(onSkipClick = onFinish)
+ },
+ footer = {
+ Button(
+ text = stringResource(CommonStrings.action_finish),
+ onClick = {
+ state.eventSink(InvitePeopleEvents.SendInvites)
+ onFinish()
},
- onNextClick = onNextClick,
+ enabled = state.canInvite,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp)
)
- }
- ) { padding ->
- UserListView(
- modifier = Modifier
- .fillMaxSize()
- .padding(padding)
- .consumeWindowInsets(padding),
- state = state,
- showBackButton = false,
- onSelectUser = {},
- onDeselectUser = {},
- )
- }
+ },
+ content = invitePeopleView
+ )
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-private fun AddPeopleViewTopBar(
- hasSelectedUsers: Boolean,
- onBackClick: () -> Unit,
- onNextClick: () -> Unit,
+private fun AddPeopleTopBar(
+ onSkipClick: () -> Unit,
) {
TopAppBar(
- titleStr = stringResource(id = R.string.screen_create_room_add_people_title),
- navigationIcon = { BackButton(onClick = onBackClick) },
+ titleStr = stringResource(R.string.screen_create_room_add_people_title),
actions = {
- val textActionResId = if (hasSelectedUsers) CommonStrings.action_next else CommonStrings.action_skip
TextButton(
- text = stringResource(id = textActionResId),
- onClick = onNextClick,
+ text = stringResource(CommonStrings.action_skip),
+ onClick = onSkipClick,
)
}
)
@@ -85,10 +76,10 @@ private fun AddPeopleViewTopBar(
@PreviewsDayNight
@Composable
-internal fun AddPeopleViewPreview(@PreviewParameter(AddPeopleUserListStateProvider::class) state: UserListState) = ElementPreview {
+internal fun AddPeopleViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview {
AddPeopleView(
state = state,
- onBackClick = {},
- onNextClick = {},
+ invitePeopleView = {},
+ onFinish = {},
)
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt
index 6885123f63..cd5470bc24 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt
@@ -7,7 +7,6 @@
package io.element.android.features.createroom.impl.configureroom
-import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.media.AvatarAction
sealed interface ConfigureRoomEvents {
@@ -16,7 +15,6 @@ sealed interface ConfigureRoomEvents {
data class RoomVisibilityChanged(val visibilityItem: RoomVisibilityItem) : ConfigureRoomEvents
data class RoomAccessChanged(val roomAccess: RoomAccessItem) : ConfigureRoomEvents
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
- data class RemoveUserFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
data object CreateRoom : ConfigureRoomEvents
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
data object CancelCreateRoom : ConfigureRoomEvents
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 e718825163..6927c51366 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt
@@ -18,19 +18,20 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.createroom.CreateRoomNavigator
-import io.element.android.features.createroom.impl.di.CreateRoomScope
-import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.services.analytics.api.AnalyticsService
-@ContributesNode(CreateRoomScope::class)
+@ContributesNode(SessionScope::class)
class ConfigureRoomNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val presenter: ConfigureRoomPresenter,
private val analyticsService: AnalyticsService,
) : Node(buildContext, plugins = plugins) {
- private val navigator = plugins().first()
+ interface Callback : Plugin {
+ fun onCreateRoomSuccess(roomId: RoomId)
+ }
init {
lifecycle.subscribe(
@@ -40,6 +41,10 @@ class ConfigureRoomNode @AssistedInject constructor(
)
}
+ private fun onCreateRoomSuccess(roomId: RoomId) {
+ plugins().forEach { it.onCreateRoomSuccess(roomId) }
+ }
+
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@@ -47,9 +52,7 @@ class ConfigureRoomNode @AssistedInject constructor(
state = state,
modifier = modifier,
onBackClick = this::navigateUp,
- onCreateRoomSuccess = {
- navigator.onOpenRoom(roomIdOrAlias = it.toRoomIdOrAlias(), serverNames = emptyList())
- },
+ onCreateRoomSuccess = ::onCreateRoomSuccess,
)
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt
index a3e8f67edf..9b2880fb6f 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt
@@ -18,8 +18,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import im.vector.app.features.analytics.plan.CreatedRoom
-import io.element.android.features.createroom.impl.CreateRoomConfig
-import io.element.android.features.createroom.impl.CreateRoomDataStore
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
@@ -50,7 +48,7 @@ import javax.inject.Inject
import kotlin.jvm.optionals.getOrDefault
class ConfigureRoomPresenter @Inject constructor(
- private val dataStore: CreateRoomDataStore,
+ private val dataStore: CreateRoomConfigStore,
private val matrixClient: MatrixClient,
private val mediaPickerProvider: PickerProvider,
private val mediaPreProcessor: MediaPreProcessor,
@@ -66,7 +64,7 @@ class ConfigureRoomPresenter @Inject constructor(
@Composable
override fun present(): ConfigureRoomState {
val cameraPermissionState = cameraPermissionPresenter.present()
- val createRoomConfig by dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig())
+ val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState(CreateRoomConfig())
val homeserverName = remember { matrixClient.userIdServerName() }
val isKnockFeatureEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
@@ -121,7 +119,6 @@ class ConfigureRoomPresenter @Inject constructor(
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
is ConfigureRoomEvents.RoomVisibilityChanged -> dataStore.setRoomVisibility(event.visibilityItem)
- is ConfigureRoomEvents.RemoveUserFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser)
is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess)
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
@@ -206,6 +203,6 @@ class ConfigureRoomPresenter @Inject constructor(
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
).getOrThrow()
val byteArray = preprocessed.file.readBytes()
- return matrixClient.uploadMedia(MimeTypes.Jpeg, byteArray, null).getOrThrow()
+ return matrixClient.uploadMedia(MimeTypes.Jpeg, byteArray).getOrThrow()
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt
index 6651d16604..90022a9204 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt
@@ -7,7 +7,6 @@
package io.element.android.features.createroom.impl.configureroom
-import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.media.AvatarAction
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt
index 71568dbbc7..7db8744408 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt
@@ -8,7 +8,6 @@
package io.element.android.features.createroom.impl.configureroom
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
index f639704890..8c56607a22 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
@@ -12,7 +12,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
@@ -58,7 +57,6 @@ import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
-import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
import io.element.android.libraries.matrix.ui.room.address.RoomAddressField
import io.element.android.libraries.permissions.api.PermissionsView
@@ -112,16 +110,6 @@ fun ConfigureRoomView(
topic = state.config.topic.orEmpty(),
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
)
- if (state.config.invites.isNotEmpty()) {
- SelectedUsersRowList(
- contentPadding = PaddingValues(horizontal = 24.dp),
- selectedUsers = state.config.invites,
- onUserRemove = {
- focusManager.clearFocus()
- state.eventSink(ConfigureRoomEvents.RemoveUserFromSelection(it))
- },
- )
- }
RoomVisibilityOptions(
selected = when (state.config.roomVisibility) {
is RoomVisibilityState.Private -> RoomVisibilityItem.Private
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomConfig.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfig.kt
similarity index 82%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomConfig.kt
rename to features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfig.kt
index 4cfa6158b8..9ec71f5b76 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomConfig.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfig.kt
@@ -5,10 +5,9 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl
+package io.element.android.features.createroom.impl.configureroom
import android.net.Uri
-import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfigStore.kt
similarity index 78%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt
rename to features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfigStore.kt
index 29b1525f51..bee9b686d6 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfigStore.kt
@@ -5,45 +5,29 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl
+package io.element.android.features.createroom.impl.configureroom
import android.net.Uri
-import io.element.android.features.createroom.impl.configureroom.RoomAccess
-import io.element.android.features.createroom.impl.configureroom.RoomAccessItem
-import io.element.android.features.createroom.impl.configureroom.RoomAddress
-import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
-import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
-import io.element.android.features.createroom.impl.di.CreateRoomScope
-import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.libraries.androidutils.file.safeDelete
-import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
-import kotlinx.collections.immutable.toImmutableList
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.getAndUpdate
import java.io.File
import javax.inject.Inject
-@SingleIn(CreateRoomScope::class)
-class CreateRoomDataStore @Inject constructor(
- val selectedUserListDataStore: UserListDataStore,
+class CreateRoomConfigStore @Inject constructor(
private val roomAliasHelper: RoomAliasHelper,
) {
private val createRoomConfigFlow: MutableStateFlow = MutableStateFlow(CreateRoomConfig())
+
private var cachedAvatarUri: Uri? = null
set(value) {
field?.path?.let { File(it) }?.safeDelete()
field = value
}
- val createRoomConfigWithInvites: Flow = combine(
- selectedUserListDataStore.selectedUsers,
- createRoomConfigFlow,
- ) { selectedUsers, config ->
- config.copy(invites = selectedUsers.toImmutableList())
- }
+ fun getCreateRoomConfigFlow(): StateFlow = createRoomConfigFlow
fun setRoomName(roomName: String) {
createRoomConfigFlow.getAndUpdate { config ->
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomComponent.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomComponent.kt
deleted file mode 100644
index b47b8c30e5..0000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomComponent.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.di
-
-import com.squareup.anvil.annotations.ContributesTo
-import com.squareup.anvil.annotations.MergeSubcomponent
-import io.element.android.libraries.architecture.NodeFactoriesBindings
-import io.element.android.libraries.di.SessionScope
-import io.element.android.libraries.di.SingleIn
-
-@SingleIn(CreateRoomScope::class)
-@MergeSubcomponent(CreateRoomScope::class)
-interface CreateRoomComponent : NodeFactoriesBindings {
- @MergeSubcomponent.Builder
- interface Builder {
- fun build(): CreateRoomComponent
- }
-
- @ContributesTo(SessionScope::class)
- interface ParentBindings {
- fun createRoomComponentBuilder(): Builder
- }
-}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt
deleted file mode 100644
index bc3c4a39b9..0000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.root
-
-import io.element.android.libraries.matrix.api.user.MatrixUser
-
-sealed interface CreateRoomRootEvents {
- data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
- data object CancelStartDM : CreateRoomRootEvents
-}
diff --git a/features/createroom/impl/src/main/res/values-be/translations.xml b/features/createroom/impl/src/main/res/values-be/translations.xml
index 8acb990b02..f5d6a234e2 100644
--- a/features/createroom/impl/src/main/res/values-be/translations.xml
+++ b/features/createroom/impl/src/main/res/values-be/translations.xml
@@ -14,6 +14,4 @@
"Назва пакоя"
"Стварыце пакой"
"Тэма (неабавязкова)"
- "Каталог пакояў"
- "Пры спробе пачаць чат адбылася памылка"
diff --git a/features/createroom/impl/src/main/res/values-bg/translations.xml b/features/createroom/impl/src/main/res/values-bg/translations.xml
index fa6dae371c..249058b7af 100644
--- a/features/createroom/impl/src/main/res/values-bg/translations.xml
+++ b/features/createroom/impl/src/main/res/values-bg/translations.xml
@@ -15,9 +15,4 @@
"Видимост на стаята"
"Създаване на стая"
"Тема за разговор (незадължително)"
- "Присъединяване към стая по адрес"
- "Не е валиден адрес"
- "Въведете…"
- "Стаята не е намерена"
- "напр. #room-name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-cs/translations.xml b/features/createroom/impl/src/main/res/values-cs/translations.xml
index 53592e0db0..e19cfbcf91 100644
--- a/features/createroom/impl/src/main/res/values-cs/translations.xml
+++ b/features/createroom/impl/src/main/res/values-cs/translations.xml
@@ -19,12 +19,4 @@ To můžete kdykoli změnit v nastavení místnosti."
"Viditelnost místnosti"
"Vytvořit místnost"
"Téma (nepovinné)"
- "Adresář místností"
- "Při pokusu o zahájení chatu došlo k chybě"
- "Vstoupit do místnosti pomocí adresy"
- "Neplatná adresa"
- "Zadejte…"
- "Odpovídající místnost nalezena"
- "Místnost nebyla nalezena"
- "např. #room-name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-cy/translations.xml b/features/createroom/impl/src/main/res/values-cy/translations.xml
index a3a9e30e35..52168014bf 100644
--- a/features/createroom/impl/src/main/res/values-cy/translations.xml
+++ b/features/createroom/impl/src/main/res/values-cy/translations.xml
@@ -19,12 +19,4 @@ Gallwch newid hyn unrhyw bryd yng ngosodiadau ystafell."
"Gwelededd yr ystafell"
"Creu ystafell"
"Pwnc (dewisol)"
- "Cyfeiriadur ystafelloedd"
- "Digwyddodd gwall wrth geisio cychwyn sgwrs"
- "Ymuno â\'r ystafell yn ôl cyfeiriad"
- "Ddim yn gyfeiriad dilys"
- "Ewch i mewn…"
- "Cafwyd hyd i ystafell gyfatebol"
- "Heb ganfod yr ystafell"
- "e.e. #enw-ystafell:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-da/translations.xml b/features/createroom/impl/src/main/res/values-da/translations.xml
index c9c182b474..a8f66f60db 100644
--- a/features/createroom/impl/src/main/res/values-da/translations.xml
+++ b/features/createroom/impl/src/main/res/values-da/translations.xml
@@ -19,12 +19,4 @@ Du kan ændre dette når som helst i rummets indstillinger."
"Rummets synlighed"
"Opret et rum"
"Emne (valgfrit)"
- "Register over rum"
- "Der opstod en fejl under forsøget på at starte en samtale"
- "Tilslut dig rummet med adressen"
- "Ikke en gyldig adresse"
- "Indtast…"
- "Matchende rum fundet"
- "Rum ikke fundet"
- "f.eks. #rummets-navn:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-de/translations.xml b/features/createroom/impl/src/main/res/values-de/translations.xml
index 90438aa38c..213d10ae7a 100644
--- a/features/createroom/impl/src/main/res/values-de/translations.xml
+++ b/features/createroom/impl/src/main/res/values-de/translations.xml
@@ -19,12 +19,4 @@ Sie können dies aber jederzeit in den Chatroomeinstellungen ändern."
" Sichtbarkeit des Chatrooms"
"Raum erstellen"
"Thema (optional)"
- "Raum-Verzeichnis"
- "Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"
- "Raum per Adresse betreten"
- "Keine gültige Adresse"
- "Eintreten…"
- "Passender Raum gefunden"
- "Raum nicht gefunden"
- "z. B. #room -name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-el/translations.xml b/features/createroom/impl/src/main/res/values-el/translations.xml
index c036b43a05..a3084a4e70 100644
--- a/features/createroom/impl/src/main/res/values-el/translations.xml
+++ b/features/createroom/impl/src/main/res/values-el/translations.xml
@@ -19,12 +19,4 @@
"Ορατότητα αίθουσας"
"Δημιουργία αίθουσας"
"Θέμα (προαιρετικό)"
- "Κατάλογος αιθουσών"
- "Παρουσιάστηκε σφάλμα κατά την προσπάθεια έναρξης μιας συνομιλίας"
- "Συμμετοχή σε αίθουσα μέσω διεύθυνσης"
- "Μη έγκυρη διεύθυνση"
- "Εισάγετε…"
- "Βρέθηκε η αντίστοιχη αίθουσα"
- "Η αίθουσα δεν βρέθηκε"
- "π.χ. #όνομα-αίθουσας:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-es/translations.xml b/features/createroom/impl/src/main/res/values-es/translations.xml
index c11f08fc14..521f761174 100644
--- a/features/createroom/impl/src/main/res/values-es/translations.xml
+++ b/features/createroom/impl/src/main/res/values-es/translations.xml
@@ -19,12 +19,4 @@ Puedes cambiar esto en cualquier momento en los ajustes de la sala."
"Visibilidad de la sala"
"Crear una sala"
"Tema (opcional)"
- "Directorio de salas"
- "Se ha producido un error al intentar iniciar un chat"
- "Unirse a una sala por su dirección"
- "Dirección no válida"
- "Introducir…"
- "Sala encontrada"
- "No se encontró la sala"
- "p. ej., #nombre-de-la-sala:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-et/translations.xml b/features/createroom/impl/src/main/res/values-et/translations.xml
index 57a26106b3..6a1d9dc58a 100644
--- a/features/createroom/impl/src/main/res/values-et/translations.xml
+++ b/features/createroom/impl/src/main/res/values-et/translations.xml
@@ -19,12 +19,4 @@ Sa võid seda jututoa seadistustest alati muuta."
"Jututoa nähtavus"
"Loo jututuba"
"Teema (kui soovid lisada)"
- "Jututubade kataloog"
- "Vestluse alustamisel tekkis viga"
- "Liitu jututoaga aadressi alusel"
- "See pole kehtiv aadress"
- "Sisene…"
- "Leidsime vastava jututoa"
- "Jututuba ei leidu"
- "nt. #jututoa-nimi:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-eu/translations.xml b/features/createroom/impl/src/main/res/values-eu/translations.xml
index 43f67e429c..537aa495a5 100644
--- a/features/createroom/impl/src/main/res/values-eu/translations.xml
+++ b/features/createroom/impl/src/main/res/values-eu/translations.xml
@@ -16,8 +16,4 @@ Gelaren ezarpenetan aldatu dezakezu hobespena."
"Gelaren ikusgarritasuna"
"Sortu gela"
"Mintzagaia (aukerakoa)"
- "Gelen direktorioa"
- "Errorea gertatu da txata hasten saiatzean"
- "Sartu…"
- "Ez da gela aurkitu"
diff --git a/features/createroom/impl/src/main/res/values-fa/translations.xml b/features/createroom/impl/src/main/res/values-fa/translations.xml
index 03bc5c3f58..09869c76f6 100644
--- a/features/createroom/impl/src/main/res/values-fa/translations.xml
+++ b/features/createroom/impl/src/main/res/values-fa/translations.xml
@@ -17,12 +17,4 @@
"نمایانی اتاق"
"ایجاد اتاق"
"موضوع (اختیاری)"
- "فهرست اتاقها"
- "هنگام تلاش برای شروع چت خطایی روی داد"
- "پیوستن به اتاق با نشانی"
- "نشانی معتبری نیست"
- "ورود…"
- "اتاق مطابق پیدا شد"
- "اتاق پیدا نشد"
- "نمونه: #room-name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-fi/translations.xml b/features/createroom/impl/src/main/res/values-fi/translations.xml
index cdb6d04d59..31589ab997 100644
--- a/features/createroom/impl/src/main/res/values-fi/translations.xml
+++ b/features/createroom/impl/src/main/res/values-fi/translations.xml
@@ -19,12 +19,4 @@ Voit muuttaa tämän milloin tahansa huoneen asetuksista."
"Huoneen näkyvyys"
"Luo huone"
"Aihe (valinnainen)"
- "Huoneluettelo"
- "Keskustelun aloituksessa tapahtui virhe"
- "Liity huoneeseen osoitteella"
- "Osoite ei ole kelvollinen"
- "Syötä…"
- "Täsmäävä huone löytyi"
- "Huonetta ei löytynyt"
- "esim. #huoneen-nimi:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-fr/translations.xml b/features/createroom/impl/src/main/res/values-fr/translations.xml
index 3f9e01dc85..afbdc919ba 100644
--- a/features/createroom/impl/src/main/res/values-fr/translations.xml
+++ b/features/createroom/impl/src/main/res/values-fr/translations.xml
@@ -19,12 +19,4 @@ Vous pouvez modifier cela à tout moment dans les paramètres du salon.""Visibilité du salon"
"Créer un salon"
"Sujet (facultatif)"
- "Annuaire des salons"
- "Une erreur s’est produite lors de la tentative de création de la discussion"
- "Saisir une adresse de salon"
- "Ce n’est pas une adresse valide"
- "Saisir…"
- "Ce salon existe"
- "Salon non trouvé"
- "ex: #nom-du-salon:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-hu/translations.xml b/features/createroom/impl/src/main/res/values-hu/translations.xml
index 1e4a721279..8833afcdec 100644
--- a/features/createroom/impl/src/main/res/values-hu/translations.xml
+++ b/features/createroom/impl/src/main/res/values-hu/translations.xml
@@ -19,12 +19,4 @@ Ezt bármikor módosíthatja a szobabeállításokban."
"Szoba láthatósága"
"Szoba létrehozása"
"Téma (nem kötelező)"
- "Szobakatalógus"
- "Hiba történt a csevegés indításakor"
- "Csatlakozás a szobához cím szerint"
- "Nem érvényes cím"
- "Írja be…"
- "Megfelelő szoba található"
- "Szoba nem található"
- "pl. #szoba-neve:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-in/translations.xml b/features/createroom/impl/src/main/res/values-in/translations.xml
index 65f135e617..219f621068 100644
--- a/features/createroom/impl/src/main/res/values-in/translations.xml
+++ b/features/createroom/impl/src/main/res/values-in/translations.xml
@@ -19,12 +19,4 @@ Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."
"Keterlihatan ruangan"
"Buat ruangan"
"Topik (opsional)"
- "Direktori ruangan"
- "Terjadi kesalahan saat mencoba memulai obrolan"
- "Bergabung dalam ruangan berdasarkan alamat"
- "Bukan alamat yang valid"
- "Masuk…"
- "Ruangan yang cocok ditemukan"
- "Ruangan tidak ditemukan"
- "mis. #nama-ruangan:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-it/translations.xml b/features/createroom/impl/src/main/res/values-it/translations.xml
index 701ebc967f..741b88b763 100644
--- a/features/createroom/impl/src/main/res/values-it/translations.xml
+++ b/features/createroom/impl/src/main/res/values-it/translations.xml
@@ -19,12 +19,4 @@ Puoi modificarlo in qualsiasi momento nelle impostazioni della stanza."
"Visibilità della stanza"
"Crea una stanza"
"Argomento (facoltativo)"
- "Elenco delle stanze"
- "Si è verificato un errore durante il tentativo di avviare una chat"
- "Accedi alla stanza tramite indirizzo"
- "Indirizzo non valido"
- "Inserisci…"
- "Stanza trovata"
- "Stanza non trovata"
- "ad esempio #room -name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-ka/translations.xml b/features/createroom/impl/src/main/res/values-ka/translations.xml
index bb4a430320..20c7af40de 100644
--- a/features/createroom/impl/src/main/res/values-ka/translations.xml
+++ b/features/createroom/impl/src/main/res/values-ka/translations.xml
@@ -10,6 +10,4 @@
"ოთახის სახელი"
"ოთახის შექმნა"
"თემა (სურვილისამებრ)"
- "ოთახის კატალოგი"
- "ჩატის დაწყების მცდელობისას შეცდომა მოხდა"
diff --git a/features/createroom/impl/src/main/res/values-lt/translations.xml b/features/createroom/impl/src/main/res/values-lt/translations.xml
index 2f7a4438ea..2fb7b3eb5c 100644
--- a/features/createroom/impl/src/main/res/values-lt/translations.xml
+++ b/features/createroom/impl/src/main/res/values-lt/translations.xml
@@ -10,5 +10,4 @@ Tai galite bet kada pakeisti kambario nustatymuose."
"Kambario pavadinimas"
"Kurti kambarį"
"Tema (nebūtina)"
- "Bandant pradėti pokalbį įvyko klaida"
diff --git a/features/createroom/impl/src/main/res/values-nb/translations.xml b/features/createroom/impl/src/main/res/values-nb/translations.xml
index 9266993fd4..e6021cf5ac 100644
--- a/features/createroom/impl/src/main/res/values-nb/translations.xml
+++ b/features/createroom/impl/src/main/res/values-nb/translations.xml
@@ -19,12 +19,4 @@ Du kan endre dette når som helst i rominnstillingene."
"Romsynlighet"
"Opprett et rom"
"Emne (valgfritt)"
- "Romkatalog"
- "Det oppstod en feil når du prøvde å starte en chat"
- "Bli med i rommet med adresse"
- "Ikke en gyldig adresse"
- "Gå inn…"
- "Matchende rom funnet"
- "Rom ikke funnet"
- "f.eks. #rom-navn:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-nl/translations.xml b/features/createroom/impl/src/main/res/values-nl/translations.xml
index 29d070a263..5142148171 100644
--- a/features/createroom/impl/src/main/res/values-nl/translations.xml
+++ b/features/createroom/impl/src/main/res/values-nl/translations.xml
@@ -16,6 +16,4 @@ Je kunt dit op elk gewenst moment wijzigen in de kamerinstellingen."
"Naam van de kamer"
"Creëer een kamer"
"Onderwerp (optioneel)"
- "Kamergids"
- "Er is een fout opgetreden bij het starten van een chat"
diff --git a/features/createroom/impl/src/main/res/values-pl/translations.xml b/features/createroom/impl/src/main/res/values-pl/translations.xml
index 3ed917b7bb..446644b622 100644
--- a/features/createroom/impl/src/main/res/values-pl/translations.xml
+++ b/features/createroom/impl/src/main/res/values-pl/translations.xml
@@ -19,12 +19,4 @@ Możesz to zmienić w ustawieniach pokoju."
"Widoczność pomieszczenia"
"Utwórz pokój"
"Temat (opcjonalnie)"
- "Katalog pokoi"
- "Wystąpił błąd podczas próby rozpoczęcia czatu"
- "Dołącz do pokoju za pomocą adresu"
- "Nieprawidłowy adres"
- "Wprowadź…"
- "Znaleziono pasujący pokój"
- "Nie znaleziono pokoju"
- "np. #room-name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-pt-rBR/translations.xml b/features/createroom/impl/src/main/res/values-pt-rBR/translations.xml
index 52c9f569af..1b1e9f2d1b 100644
--- a/features/createroom/impl/src/main/res/values-pt-rBR/translations.xml
+++ b/features/createroom/impl/src/main/res/values-pt-rBR/translations.xml
@@ -19,12 +19,4 @@ Você pode mudar isso a qualquer momento nas configurações da sala."
"Visibilidade da sala"
"Criar uma sala"
"Tópico (opcional)"
- "Diretório de salas"
- "Ocorreu um erro ao tentar iniciar um chat"
- "Entrar na sala pelo endereço"
- "Não é um endereço válido"
- "Entrar…"
- "Foi encontrada uma sala correspondente"
- "Sala não encontrada"
- "Por exemplo, #nome-da-sala:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-pt/translations.xml b/features/createroom/impl/src/main/res/values-pt/translations.xml
index 5e5f5fc9c3..1524914bb2 100644
--- a/features/createroom/impl/src/main/res/values-pt/translations.xml
+++ b/features/createroom/impl/src/main/res/values-pt/translations.xml
@@ -19,12 +19,4 @@ Pode alterar esta opção nas definições da sala."
"Visibilidade da sala"
"Criar uma sala"
"Descrição (opcional)"
- "Diretório de salas"
- "Ocorreu um erro ao tentar iniciar uma conversa"
- "Entrar na sala pelo endereço"
- "Não é um endereço válido"
- "Entrar…"
- "Sala correspondente encontrado"
- "Sala não encontrada"
- "por exemplo, #sala:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-ro/translations.xml b/features/createroom/impl/src/main/res/values-ro/translations.xml
index 8d9e74b530..a5ea4fbb85 100644
--- a/features/createroom/impl/src/main/res/values-ro/translations.xml
+++ b/features/createroom/impl/src/main/res/values-ro/translations.xml
@@ -17,6 +17,4 @@ Puteți modifica acest lucru oricând în setări."
"Numele camerei"
"Creați o cameră"
"Subiect (opțional)"
- "Director de camere"
- "A apărut o eroare la încercarea începerii conversației"
diff --git a/features/createroom/impl/src/main/res/values-ru/translations.xml b/features/createroom/impl/src/main/res/values-ru/translations.xml
index 6344491248..e871673114 100644
--- a/features/createroom/impl/src/main/res/values-ru/translations.xml
+++ b/features/createroom/impl/src/main/res/values-ru/translations.xml
@@ -19,12 +19,4 @@
"Видимость комнаты"
"Создать комнату"
"Тема (необязательно)"
- "Каталог комнат"
- "Произошла ошибка при запуске чата"
- "Присоединиться к комнате по адресу"
- "Недействительный адрес"
- "Ввести…"
- "Соответствующая комната найдена"
- "Комната не найдена"
- "прим. #room-name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-sk/translations.xml b/features/createroom/impl/src/main/res/values-sk/translations.xml
index eaf6c2f19c..7b6d89b2e1 100644
--- a/features/createroom/impl/src/main/res/values-sk/translations.xml
+++ b/features/createroom/impl/src/main/res/values-sk/translations.xml
@@ -19,12 +19,4 @@ Môžete to kedykoľvek zmeniť v nastaveniach miestnosti."
"Viditeľnosť miestnosti"
"Vytvoriť miestnosť"
"Téma (voliteľné)"
- "Adresár miestností"
- "Pri pokuse o spustenie konverzácie sa vyskytla chyba"
- "Pripojte sa do miestnosti podľa adresy"
- "Neplatná adresa"
- "Zadajte…"
- "Nájdená zodpovedajúca miestnosť"
- "Miestnosť sa nenašla"
- "napr. #nazov-miestnosti:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-sv/translations.xml b/features/createroom/impl/src/main/res/values-sv/translations.xml
index 316c9cc32e..8cd01ebd0e 100644
--- a/features/createroom/impl/src/main/res/values-sv/translations.xml
+++ b/features/createroom/impl/src/main/res/values-sv/translations.xml
@@ -19,12 +19,4 @@ Du kan ändra detta när som helst i rumsinställningarna."
"Rumssynlighet"
"Skapa ett rum"
"Ämne (valfritt)"
- "Rumskatalog"
- "Ett fel uppstod när du försökte starta en chatt"
- "Gå med i rum med adress"
- "Inte en giltig adress"
- "Ange …"
- "Matchande rum hittades"
- "Rummet hittades inte"
- "t.ex. #rumsnamn:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-tr/translations.xml b/features/createroom/impl/src/main/res/values-tr/translations.xml
index c8e3136517..d97139c973 100644
--- a/features/createroom/impl/src/main/res/values-tr/translations.xml
+++ b/features/createroom/impl/src/main/res/values-tr/translations.xml
@@ -19,6 +19,4 @@ Bunu istediğiniz zaman oda ayarlarından değiştirebilirsiniz."
"Oda görünürlüğü"
"Bir oda oluştur"
"Konu (isteğe bağlı)"
- "Oda dizini"
- "Sohbet başlatmaya çalışırken bir hata oluştu"
diff --git a/features/createroom/impl/src/main/res/values-uk/translations.xml b/features/createroom/impl/src/main/res/values-uk/translations.xml
index a43172fb8b..047b4dd9d2 100644
--- a/features/createroom/impl/src/main/res/values-uk/translations.xml
+++ b/features/createroom/impl/src/main/res/values-uk/translations.xml
@@ -19,12 +19,4 @@
"Видимість кімнати"
"Створити кімнату"
"Тема (необов\'язково)"
- "Каталог кімнат"
- "Під час спроби почати бесіду сталася помилка"
- "Приєднатися до кімнати за адресою"
- "Недійсна адреса"
- "Введіть…"
- "Знайдено відповідну кімнату"
- "Кімната не знайдена"
- "наприклад, #room-name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-ur/translations.xml b/features/createroom/impl/src/main/res/values-ur/translations.xml
index 6fe476462e..b68992085f 100644
--- a/features/createroom/impl/src/main/res/values-ur/translations.xml
+++ b/features/createroom/impl/src/main/res/values-ur/translations.xml
@@ -11,6 +11,4 @@
"کمرے کا نام"
"ایک کمرہ بنائیں"
"موضوع (اختیاری)"
- "کمرے کا راہنامچہ"
- "گفتگو شروع کرنے کی کوشش کرتے وقت ایک خرابی واقع ہوگئی"
diff --git a/features/createroom/impl/src/main/res/values-uz/translations.xml b/features/createroom/impl/src/main/res/values-uz/translations.xml
index e3bc9de528..c2591ff235 100644
--- a/features/createroom/impl/src/main/res/values-uz/translations.xml
+++ b/features/createroom/impl/src/main/res/values-uz/translations.xml
@@ -3,11 +3,11 @@
"Yangi xona"
"Odamlarni taklif qiling"
"Xonani yaratishda xatolik yuz berdi"
- "Bu xonadagi xabarlar shifrlangan. Keyinchalik shifrlashni o‘chirib bo‘lmaydi."
- "Shaxsiy xona (faqat taklif)"
- "Xabarlar shifrlanmagan va har kim ularni o\'qiy oladi. Keyinchalik shifrlashni yoqishingiz mumkin."
+ "Faqat taklif etilgan shaxslargina bu xonaga kira oladi. Barcha xabarlar boshdan-oxirigacha shifrlanadi."
+ "Shaxsiy xona"
+ "Bu xonani har kim topishi mumkin.
+Buni xona sozlamalaridan istalgan vaqtda oʻzgartirishingiz mumkin."
"Xona nomi"
"Xonani yaratish"
"Mavzu (ixtiyoriy)"
- "Suhbatni boshlashda xatolik yuz berdi"
diff --git a/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml b/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml
index 0d67c883c7..476f9cff7e 100644
--- a/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml
@@ -19,12 +19,4 @@
"聊天室能見度"
"建立聊天室"
"主題(非必填)"
- "聊天室目錄"
- "嘗試開始聊天時發生錯誤"
- "按地址加入聊天室"
- "不是有效的位址"
- "輸入……"
- "找到相符的聊天室"
- "找不到聊天室"
- "例如 #room-name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-zh/translations.xml b/features/createroom/impl/src/main/res/values-zh/translations.xml
index 24d7df7f35..d20e34801d 100644
--- a/features/createroom/impl/src/main/res/values-zh/translations.xml
+++ b/features/createroom/impl/src/main/res/values-zh/translations.xml
@@ -19,6 +19,4 @@
"房间可见性"
"创建聊天室"
"主题(可选)"
- "聊天室目录"
- "在开始聊天时发生了错误"
diff --git a/features/createroom/impl/src/main/res/values/localazy.xml b/features/createroom/impl/src/main/res/values/localazy.xml
index d011a1d87b..fa9a1cb276 100644
--- a/features/createroom/impl/src/main/res/values/localazy.xml
+++ b/features/createroom/impl/src/main/res/values/localazy.xml
@@ -19,12 +19,4 @@ You can change this anytime in room settings."
"Room visibility"
"Create a room"
"Topic (optional)"
- "Room directory"
- "An error occurred when trying to start a chat"
- "Join room by address"
- "Not a valid address"
- "Enter…"
- "Matching room found"
- "Room not found"
- "e.g. #room-name:matrix.org"
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTest.kt
deleted file mode 100644
index acb381dadc..0000000000
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTest.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.addpeople
-
-import app.cash.molecule.RecompositionMode
-import app.cash.molecule.moleculeFlow
-import app.cash.turbine.test
-import com.google.common.truth.Truth.assertThat
-import io.element.android.features.createroom.impl.CreateRoomDataStore
-import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
-import io.element.android.features.createroom.impl.userlist.UserListDataStore
-import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
-import io.element.android.libraries.usersearch.test.FakeUserRepository
-import io.element.android.tests.testutils.WarmUpRule
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-
-class AddPeoplePresenterTest {
- @get:Rule
- val warmUpRule = WarmUpRule()
-
- private lateinit var presenter: AddPeoplePresenter
-
- @Before
- fun setup() {
- presenter = AddPeoplePresenter(
- FakeUserListPresenterFactory(),
- FakeUserRepository(),
- CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper())
- )
- }
-
- @Test
- fun `present - initial state`() = runTest {
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- // TODO This doesn't actually test anything...
- val initialState = awaitItem()
- assertThat(initialState)
- }
- }
-}
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleViewTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleViewTest.kt
deleted file mode 100644
index c18abf0daa..0000000000
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleViewTest.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.addpeople
-
-import androidx.activity.ComponentActivity
-import androidx.compose.ui.test.junit4.AndroidComposeTestRule
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import io.element.android.features.createroom.impl.userlist.UserListEvents
-import io.element.android.features.createroom.impl.userlist.UserListState
-import io.element.android.features.createroom.impl.userlist.aUserListState
-import io.element.android.libraries.ui.strings.CommonStrings
-import io.element.android.tests.testutils.EnsureNeverCalled
-import io.element.android.tests.testutils.EventsRecorder
-import io.element.android.tests.testutils.clickOn
-import io.element.android.tests.testutils.ensureCalledOnce
-import io.element.android.tests.testutils.pressBack
-import org.junit.Rule
-import org.junit.Test
-import org.junit.rules.TestRule
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class AddPeopleViewTest {
- @get:Rule
- val rule = createAndroidComposeRule()
-
- @Test
- fun `clicking on back invokes the expected callback`() {
- val eventsRecorder = EventsRecorder()
- ensureCalledOnce {
- rule.setAddPeopleView(
- aUserListState(
- eventSink = eventsRecorder,
- ),
- onBackClick = it
- )
- rule.pressBack()
- }
- eventsRecorder.assertSingle(UserListEvents.UpdateSearchQuery(""))
- }
-
- @Test
- fun `clicking on back during search emits the expected Event`() {
- val eventsRecorder = EventsRecorder()
- rule.setAddPeopleView(
- aUserListState(
- isSearchActive = true,
- eventSink = eventsRecorder,
- ),
- )
- rule.pressBack()
- eventsRecorder.assertSingle(UserListEvents.OnSearchActiveChanged(false))
- }
-
- @Test
- fun `clicking on skip invokes the expected callback`() {
- val eventsRecorder = EventsRecorder()
- ensureCalledOnce {
- rule.setAddPeopleView(
- aUserListState(
- eventSink = eventsRecorder,
- ),
- onNextClick = it
- )
- rule.clickOn(CommonStrings.action_skip)
- }
- eventsRecorder.assertSingle(UserListEvents.UpdateSearchQuery(""))
- }
-}
-
-private fun AndroidComposeTestRule.setAddPeopleView(
- state: UserListState,
- onBackClick: () -> Unit = EnsureNeverCalled(),
- onNextClick: () -> Unit = EnsureNeverCalled(),
-) {
- setContent {
- AddPeopleView(
- state = state,
- onBackClick = onBackClick,
- onNextClick = onNextClick,
- )
- }
-}
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureBaseRoomPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/startchat/impl/configureroom/ConfigureRoomPresenterTest.kt
similarity index 90%
rename from features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureBaseRoomPresenterTest.kt
rename to features/createroom/impl/src/test/kotlin/io/element/android/features/startchat/impl/configureroom/ConfigureRoomPresenterTest.kt
index 8c8ba7ca58..635a2b330e 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureBaseRoomPresenterTest.kt
+++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/startchat/impl/configureroom/ConfigureRoomPresenterTest.kt
@@ -5,15 +5,21 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.configureroom
+package io.element.android.features.startchat.impl.configureroom
import android.net.Uri
import app.cash.turbine.TurbineTestContext
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.CreatedRoom
-import io.element.android.features.createroom.impl.CreateRoomConfig
-import io.element.android.features.createroom.impl.CreateRoomDataStore
-import io.element.android.features.createroom.impl.userlist.UserListDataStore
+import io.element.android.features.createroom.impl.configureroom.ConfigureRoomEvents
+import io.element.android.features.createroom.impl.configureroom.ConfigureRoomPresenter
+import io.element.android.features.createroom.impl.configureroom.ConfigureRoomState
+import io.element.android.features.createroom.impl.configureroom.CreateRoomConfig
+import io.element.android.features.createroom.impl.configureroom.CreateRoomConfigStore
+import io.element.android.features.createroom.impl.configureroom.RoomAccess
+import io.element.android.features.createroom.impl.configureroom.RoomAddress
+import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
+import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
@@ -28,7 +34,6 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
-import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
import io.element.android.libraries.mediapickers.api.PickerProvider
@@ -48,8 +53,6 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
-import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
@@ -67,7 +70,7 @@ private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2"
private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery"
@RunWith(RobolectricTestRunner::class)
-class ConfigureBaseRoomPresenterTest {
+class ConfigureRoomPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@@ -124,12 +127,11 @@ class ConfigureBaseRoomPresenterTest {
@Test
fun `present - state is updated when fields are changed`() = runTest {
- val userListDataStore = UserListDataStore()
val pickerProvider = FakePickerProvider()
val permissionsPresenter = FakePermissionsPresenter()
val roomAliasHelper = FakeRoomAliasHelper()
val presenter = createConfigureRoomPresenter(
- createRoomDataStore = CreateRoomDataStore(userListDataStore, roomAliasHelper),
+ dataStore = CreateRoomConfigStore(roomAliasHelper),
pickerProvider = pickerProvider,
permissionsPresenter = permissionsPresenter,
)
@@ -137,20 +139,9 @@ class ConfigureBaseRoomPresenterTest {
val initialState = initialState()
var expectedConfig = CreateRoomConfig()
assertThat(initialState.config).isEqualTo(expectedConfig)
-
- // Select User
- val selectedUser1 = aMatrixUser()
- val selectedUser2 = aMatrixUser("@id_of_bob:server.org", "Bob")
- userListDataStore.selectUser(selectedUser1)
- skipItems(1)
- userListDataStore.selectUser(selectedUser2)
- var newState = awaitItem()
- expectedConfig = expectedConfig.copy(invites = persistentListOf(selectedUser1, selectedUser2))
- assertThat(newState.config).isEqualTo(expectedConfig)
-
// Room name
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
- newState = awaitItem()
+ var newState = awaitItem()
expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME)
assertThat(newState.config).isEqualTo(expectedConfig)
@@ -206,12 +197,6 @@ class ConfigureBaseRoomPresenterTest {
)
)
assertThat(newState.config).isEqualTo(expectedConfig)
-
- // Remove user
- newState.eventSink(ConfigureRoomEvents.RemoveUserFromSelection(selectedUser1))
- newState = awaitItem()
- expectedConfig = expectedConfig.copy(invites = expectedConfig.invites.minus(selectedUser1).toImmutableList())
- assertThat(newState.config).isEqualTo(expectedConfig)
}
}
@@ -263,16 +248,16 @@ class ConfigureBaseRoomPresenterTest {
val matrixClient = createMatrixClient()
val analyticsService = FakeAnalyticsService()
val mediaPreProcessor = FakeMediaPreProcessor()
- val createRoomDataStore = CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper())
+ val dataStore = CreateRoomConfigStore(FakeRoomAliasHelper())
val presenter = createConfigureRoomPresenter(
- createRoomDataStore = createRoomDataStore,
+ dataStore = dataStore,
mediaPreProcessor = mediaPreProcessor,
matrixClient = matrixClient,
analyticsService = analyticsService
)
presenter.test {
val initialState = initialState()
- createRoomDataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY))
+ dataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY))
skipItems(1)
mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk())))
matrixClient.givenUploadMediaResult(Result.failure(AN_EXCEPTION))
@@ -405,7 +390,7 @@ class ConfigureBaseRoomPresenterTest {
private fun createConfigureRoomPresenter(
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
- createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore(), roomAliasHelper),
+ dataStore: CreateRoomConfigStore = CreateRoomConfigStore(roomAliasHelper),
matrixClient: MatrixClient = createMatrixClient(),
pickerProvider: PickerProvider = FakePickerProvider(),
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
@@ -414,7 +399,7 @@ class ConfigureBaseRoomPresenterTest {
isKnockFeatureEnabled: Boolean = true,
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
) = ConfigureRoomPresenter(
- dataStore = createRoomDataStore,
+ dataStore = dataStore,
matrixClient = matrixClient,
mediaPickerProvider = pickerProvider,
mediaPreProcessor = mediaPreProcessor,
diff --git a/features/deactivation/impl/src/main/res/values-ur/translations.xml b/features/deactivation/impl/src/main/res/values-ur/translations.xml
new file mode 100644
index 0000000000..297b29c519
--- /dev/null
+++ b/features/deactivation/impl/src/main/res/values-ur/translations.xml
@@ -0,0 +1,14 @@
+
+
+ "براہ کرم تصدیق کریں کہ آپ اپنا اکاؤنٹ غیر فعال کرنا چاہتے ہیں۔ اس کارروائی کو کالعدم نہیں کیا جا سکتا۔"
+ "میرے تمام پیغامات ڈیلیٹ کریں۔"
+ "انتباہ: مستقبل کے صارفین نامکمل گفتگو دیکھ سکتے ہیں۔"
+ "اپنے اکاؤنٹ کو غیر فعال کرنا %1$s ہے، یہ کرے گا:"
+ "ناقابل واپسی"
+ "%1$s آپ کا اکاؤنٹ (آپ دوبارہ لاگ ان نہیں ہو سکتے، اور آپ کی ID کو دوبارہ استعمال نہیں کیا جا سکتا)۔"
+ "مستقل طور پر غیر فعال کریں"
+ "آپ کو تمام چیت رومز سے ہٹا دے گا۔"
+ "ہمارے شناختی سرور سے اپنے اکاؤنٹ کی معلومات کو حذف کریں۔"
+ "آپ کے پیغامات اب بھی رجسٹرڈ صارفین کو نظر آئیں گے لیکن اگر آپ انہیں حذف کرنے کا انتخاب کرتے ہیں تو نئے یا غیر رجسٹرڈ صارفین کے لیے دستیاب نہیں ہوں گے۔"
+ "اکاؤنٹ کو غیر فعال کریں"
+
diff --git a/features/deactivation/impl/src/main/res/values-uz/translations.xml b/features/deactivation/impl/src/main/res/values-uz/translations.xml
new file mode 100644
index 0000000000..07a873d2e3
--- /dev/null
+++ b/features/deactivation/impl/src/main/res/values-uz/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Hisobni faolsizlantirish"
+
diff --git a/features/ftue/test/build.gradle.kts b/features/ftue/test/build.gradle.kts
index 396c7467b6..9c0d4ffadd 100644
--- a/features/ftue/test/build.gradle.kts
+++ b/features/ftue/test/build.gradle.kts
@@ -1,5 +1,3 @@
-import extension.setupAnvil
-
/*
* Copyright 2024 New Vector Ltd.
*
@@ -16,8 +14,6 @@ android {
namespace = "io.element.android.features.ftue.test"
}
-setupAnvil()
-
dependencies {
implementation(projects.features.ftue.api)
implementation(projects.tests.testutils)
diff --git a/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt b/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt
index 0f6ee581bb..88b55d4e02 100644
--- a/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt
+++ b/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt
@@ -22,7 +22,7 @@ interface HomeEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun onRoomClick(roomId: RoomId)
- fun onCreateRoomClick()
+ fun onStartChatClick()
fun onSettingsClick()
fun onSetUpRecoveryClick()
fun onSessionConfirmRecoveryKeyClick()
diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts
index 972c0817e2..e3c8321cd5 100644
--- a/features/home/impl/build.gradle.kts
+++ b/features/home/impl/build.gradle.kts
@@ -38,7 +38,7 @@ dependencies {
implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.eventformatter.api)
implementation(projects.libraries.indicator.api)
- implementation(projects.libraries.deeplink)
+ implementation(projects.libraries.deeplink.api)
implementation(projects.libraries.fullscreenintent.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.permissions.noop)
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 54c58d7387..4265114e24 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
@@ -44,7 +44,7 @@ import io.element.android.features.reportroom.api.ReportRoomEntryPoint
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.appyx.launchMolecule
-import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
+import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
@@ -120,8 +120,8 @@ class HomeFlowNode @AssistedInject constructor(
plugins().forEach { it.onSettingsClick() }
}
- private fun onCreateRoomClick() {
- plugins().forEach { it.onCreateRoomClick() }
+ private fun onStartChatClick() {
+ plugins().forEach { it.onStartChatClick() }
}
private fun onSetUpRecoveryClick() {
@@ -171,7 +171,7 @@ class HomeFlowNode @AssistedInject constructor(
homeState = state,
onRoomClick = this::onRoomClick,
onSettingsClick = this::onOpenSettings,
- onCreateRoomClick = this::onCreateRoomClick,
+ onStartChatClick = this::onStartChatClick,
onSetUpRecoveryClick = this::onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = this::onSessionConfirmRecoveryKeyClick,
onRoomSettingsClick = this::onRoomSettingsClick,
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 c2ef2e4bc5..a0468aa32d 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
@@ -31,6 +31,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.hazeEffect
@@ -72,7 +73,7 @@ fun HomeView(
onSettingsClick: () -> Unit,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
- onCreateRoomClick: () -> Unit,
+ onStartChatClick: () -> Unit,
onRoomSettingsClick: (roomId: RoomId) -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
onReportRoomClick: (roomId: RoomId) -> Unit,
@@ -116,7 +117,7 @@ fun HomeView(
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
onOpenSettings = { if (firstThrottler.canHandle()) onSettingsClick() },
- onCreateRoomClick = { if (firstThrottler.canHandle()) onCreateRoomClick() },
+ onStartChatClick = { if (firstThrottler.canHandle()) onStartChatClick() },
onMenuActionClick = onMenuActionClick,
modifier = Modifier.padding(top = topPadding),
)
@@ -145,7 +146,7 @@ private fun HomeScaffold(
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomId) -> Unit,
onOpenSettings: () -> Unit,
- onCreateRoomClick: () -> Unit,
+ onStartChatClick: () -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -236,7 +237,7 @@ private fun HomeScaffold(
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = ::onRoomClick,
- onCreateRoomClick = onCreateRoomClick,
+ onCreateRoomClick = onStartChatClick,
contentPadding = PaddingValues(
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80,
// and include provided bottom padding
@@ -280,7 +281,7 @@ private fun HomeScaffold(
floatingActionButton = {
if (state.displayActions) {
FloatingActionButton(
- onClick = onCreateRoomClick,
+ onClick = onStartChatClick,
) {
Icon(
imageVector = CompoundIcons.Plus(),
@@ -304,7 +305,26 @@ internal fun HomeViewPreview(@PreviewParameter(HomeStateProvider::class) state:
onSettingsClick = {},
onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {},
- onCreateRoomClick = {},
+ onStartChatClick = {},
+ onRoomSettingsClick = {},
+ onReportRoomClick = {},
+ onMenuActionClick = {},
+ onDeclineInviteAndBlockUser = {},
+ acceptDeclineInviteView = {},
+ leaveRoomView = {}
+ )
+}
+
+@Preview
+@Composable
+internal fun HomeViewA11yPreview() = ElementPreview {
+ HomeView(
+ homeState = aHomeState(),
+ onRoomClick = {},
+ onSettingsClick = {},
+ onSetUpRecoveryClick = {},
+ onConfirmRecoveryKeyClick = {},
+ onStartChatClick = {},
onRoomSettingsClick = {},
onReportRoomClick = {},
onMenuActionClick = {},
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 f6e94e85b2..fff8ce0b1e 100644
--- a/features/home/impl/src/main/res/values-da/translations.xml
+++ b/features/home/impl/src/main/res/values-da/translations.xml
@@ -13,6 +13,7 @@
"For at sikre, at du aldrig går glip af et vigtigt opkald, skal du ændre dine indstillinger til at tillade underretninger i fuld skærm, når din telefon er låst."
"Gør din opkaldsoplevelse bedre"
"Samtaler"
+ "Klynger"
"Er du sikker på, at du vil afvise invitationen til at deltage i %1$s?"
"Afvis invitation"
"Er du sikker på, at du vil afvise denne private samtale med %1$s?"
@@ -32,6 +33,7 @@ For nu kan du fravælge filtre for at se dine andre samtaler"
"Invitationer"
"Du har ingen afventende invitationer."
"Lav prioritet"
+ "Du har endnu ingen chats med lav prioritet"
"Du kan fravælge filtre for at se dine andre samtaler"
"Du har ingen samtaler til dette valg"
"Mennesker"
diff --git a/features/home/impl/src/main/res/values-et/translations.xml b/features/home/impl/src/main/res/values-et/translations.xml
index 39e099adba..43dd57cdfa 100644
--- a/features/home/impl/src/main/res/values-et/translations.xml
+++ b/features/home/impl/src/main/res/values-et/translations.xml
@@ -33,6 +33,7 @@ Aga seni… oma teiste vestluste nägemiseks pead eemaldama filtrid"
"Kutsed"
"Sul pole ootel kutseid."
"Vähetähtis"
+ "Sul pole veel ühtegi olulist vestlust"
"Oma teiste vestluste nägemiseks sa pead filtrid eemaldama"
"Selle valiku jaoks sul veel pole vestlusi"
"Inimesed"
diff --git a/features/home/impl/src/main/res/values-eu/translations.xml b/features/home/impl/src/main/res/values-eu/translations.xml
index 62d65d9949..b1eaf7749d 100644
--- a/features/home/impl/src/main/res/values-eu/translations.xml
+++ b/features/home/impl/src/main/res/values-eu/translations.xml
@@ -1,5 +1,8 @@
+ "Desgaitu bateriaren optimizazioa aplikazio honentzat, ziurtatzeko jakinarazpen guztiak jasoko direla."
+ "Desgaitu optimizazioa"
+ "Jakinarazpenak ez dira iristen?"
"Konfiguratu berreskurapena"
"Sartu zure berreskuratze-gakoa"
"Berreskuratze-gakoa ahaztu al duzu?"
@@ -15,6 +18,7 @@
"Behin egin beharreko prozesua da; eskerrik asko itxaroteagatik."
"Zure kontua konfiguratzen."
"Sortu elkarrizketa edo gela berria"
+ "Garbitu iragazkiak"
"Hasi norbaiti mezuak bidaltzen."
"Oraindik ez dago txatik."
"Gogokoak"
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 23d621a3a4..540eb3e887 100644
--- a/features/home/impl/src/main/res/values-fr/translations.xml
+++ b/features/home/impl/src/main/res/values-fr/translations.xml
@@ -13,6 +13,7 @@
"Afin de ne jamais manquer un appel important, veuillez modifier vos paramètres pour autoriser les notifications en plein écran lorsque votre appareil est verrouillé."
"Améliorez votre expérience d’appel"
"Conversations"
+ "Espaces"
"Êtes-vous sûr de vouloir décliner l’invitation à rejoindre %1$s ?"
"Refuser l’invitation"
"Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?"
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 9d7ee11427..8d498d08a6 100644
--- a/features/home/impl/src/main/res/values-hu/translations.xml
+++ b/features/home/impl/src/main/res/values-hu/translations.xml
@@ -33,6 +33,7 @@ Egyelőre törölheti a szűrőket a többi csevegés megtekintéséhez.""Meghívások"
"Nincsenek függőben lévő meghívásai."
"Alacsony prioritás"
+ "Még nincsenek alacsony prioritású csevegései"
"Kikapcsolhatja a szűrőket a többi csevegés megtekintéséhez"
"Ehhez a kiválasztáshoz nem tartoznak csevegések"
"Emberek"
diff --git a/features/home/impl/src/main/res/values-sk/translations.xml b/features/home/impl/src/main/res/values-sk/translations.xml
index 62df718bc2..f44e294432 100644
--- a/features/home/impl/src/main/res/values-sk/translations.xml
+++ b/features/home/impl/src/main/res/values-sk/translations.xml
@@ -33,6 +33,7 @@ Zatiaľ môžete zrušiť výber filtrov, aby ste videli ostatné konverzácie"<
"Pozvánky"
"Nemáte žiadne čakajúce pozvánky."
"Nízka priorita"
+ "Zatiaľ nemáte žiadne konverzácie s nízkou prioritou."
"Môžete zrušiť výber filtrov, aby ste videli svoje ostatné konverzácie"
"Nemáte konverzácie pre tento výber"
"Ľudia"
diff --git a/features/home/impl/src/main/res/values-sv/translations.xml b/features/home/impl/src/main/res/values-sv/translations.xml
index ff2d495b3a..d92b351118 100644
--- a/features/home/impl/src/main/res/values-sv/translations.xml
+++ b/features/home/impl/src/main/res/values-sv/translations.xml
@@ -13,6 +13,7 @@
"För att säkerställa att du aldrig missar ett viktigt samtal, ändra dina inställningar för att tillåta helskärmsmeddelanden när telefonen är låst."
"Förbättra din samtalsupplevelse"
"Alla chattar"
+ "Utrymmen"
"Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?"
"Avböj inbjudan"
"Är du säker på att du vill avböja denna privata chatt med %1$s?"
@@ -32,6 +33,7 @@ För tillfället kan du avmarkera filter för att se dina andra chattar""Inbjudningar"
"Du har inga väntande inbjudningar."
"Låg prioritet"
+ "Du har inga lågprioriterade chattar ännu"
"Du kan avmarkera filter för att se dina andra chattar"
"Du har inga chattar för det här valet"
"Personer"
diff --git a/features/home/impl/src/main/res/values-uk/translations.xml b/features/home/impl/src/main/res/values-uk/translations.xml
index c513aee351..f41f077df0 100644
--- a/features/home/impl/src/main/res/values-uk/translations.xml
+++ b/features/home/impl/src/main/res/values-uk/translations.xml
@@ -13,6 +13,7 @@
"Щоб ніколи не пропустити важливий виклик, змініть налаштування, щоб увімкнути повноекранні сповіщення, коли телефон заблоковано."
"Покращуйте досвід дзвінків"
"Бесіди"
+ "Простори"
"Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?"
"Відхилити запрошення"
"Ви дійсно хочете відмовитися від приватної бесіди з %1$s?"
@@ -32,6 +33,7 @@
"Запрошення"
"У вас немає запрошень, що очікують на розгляд."
"Низький пріоритет"
+ "У вас ще немає неважливих бесід"
"Ви можете зняти фільтри, щоб побачити інші ваші бесіди"
"Ви не маєте бесід для цієї категорії"
"Люди"
diff --git a/features/home/impl/src/main/res/values-ur/translations.xml b/features/home/impl/src/main/res/values-ur/translations.xml
index cdcc9661b2..78166c34c7 100644
--- a/features/home/impl/src/main/res/values-ur/translations.xml
+++ b/features/home/impl/src/main/res/values-ur/translations.xml
@@ -1,6 +1,8 @@
+ "اگر آپ اپنے تمام موجودہ آلات کھو چکے ہیں تو ایک recovery key کے ذریعہ اپنی کرپٹوگرافک شناخت اور پیغام کی سرگزشت کو دوبارہ حاصل کریں۔"
"بازیابی مرتب کریں"
+ "اپنے اکاؤنٹ کی حفاظت کے لیے ریکوری طے کریں"
"اپنے کلید کے ذخیرہ اور پیغام کی سرگزشت تک رسائی کو برقرار رکھنے کیلئے اپنی بازیابی کلید کی تصدیق کریں۔"
"آپ کا کلید کا ذخیرہ غیر ہم وقت ساز ہے۔"
"اس بات کو یقینی بنانے کے لیے کہ آپ کبھی بھی اہم مکالمہ سے محروم نہ ہوں، براہ کرم اپنی ترتیبات تبدیل کریں تاکہ آپ کا ہاتف مقفل ہونے پر مکمل پردۂ نمائش اطلاعات کی اجازت دی جا سکے۔"
diff --git a/features/home/impl/src/main/res/values-uz/translations.xml b/features/home/impl/src/main/res/values-uz/translations.xml
index e638e86838..c28d48da80 100644
--- a/features/home/impl/src/main/res/values-uz/translations.xml
+++ b/features/home/impl/src/main/res/values-uz/translations.xml
@@ -1,6 +1,10 @@
+ "Ushbu ilova uchun quvvatni optimallashtirishni oʻchirib qoʻying, barcha xabarnomalar qabul qilinganligiga ishonch hosil qilish uchun."
+ "Optimallashtirishni o\'chiring"
+ "Bildirishnoma kelmayaptimi?"
"Qayta tiklashni sozlang"
+ "Hisobingizni himoya qilish uchun tiklashni sozlang"
"Suhbatlar"
"Haqiqatan ham qo\'shilish taklifini rad qilmoqchimisiz%1$s ?"
"Taklifni rad etish"
diff --git a/features/home/impl/src/main/res/values-zh-rTW/translations.xml b/features/home/impl/src/main/res/values-zh-rTW/translations.xml
index 644baf8f08..a9f1c932a0 100644
--- a/features/home/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/home/impl/src/main/res/values-zh-rTW/translations.xml
@@ -1,5 +1,8 @@
+ "停用此應用程式的電池最佳化,才能確保收到所有通知。"
+ "停用最佳化"
+ "沒收到通知?"
"若您遺失了所有現有裝置,則請使用復原金鑰以救援您的密碼學身份與訊息歷史紀錄。"
"設定復原"
"設定備援以保護您的帳號"
@@ -10,6 +13,7 @@
"為確保您永遠不會錯過重要通話,請變更設定以允許在手機鎖定時允許全螢幕通知。"
"提升您的通話體驗"
"所有聊天室"
+ "空間"
"您確定您想要拒絕加入 %1$s 的邀請嗎?"
"拒絕邀請"
"您確定您要拒絕此與 %1$s 的私人聊天嗎?"
@@ -19,6 +23,7 @@
"這是一次性的程序,感謝您耐心等候。"
"正在設定您的帳號。"
"建立新的對話或聊天室"
+ "清除篩選條件"
"從向某人傳送訊息開始。"
"尚無聊天室。"
"我的最愛"
@@ -41,6 +46,7 @@
"所有聊天室"
"標為已讀"
"標為未讀"
+ "此聊天室已升級"
"您似乎正在使用新的裝置。請使用另一個裝置進行驗證,以存取您的加密訊息。"
"驗證這是您本人"
diff --git a/features/home/impl/src/main/res/values-zh/translations.xml b/features/home/impl/src/main/res/values-zh/translations.xml
index 52ef5ad761..7c74d11302 100644
--- a/features/home/impl/src/main/res/values-zh/translations.xml
+++ b/features/home/impl/src/main/res/values-zh/translations.xml
@@ -1,5 +1,6 @@
+ "请关闭本应用的电池优化设置,确保不错过任何消息通知。"
"禁用优化"
"通知未送达?"
"生成新的恢复密钥,该密钥可用于在您无法访问设备时恢复加密的消息历史记录。"
diff --git a/features/home/impl/src/main/res/values/localazy.xml b/features/home/impl/src/main/res/values/localazy.xml
index 4918260281..e6e09b5e47 100644
--- a/features/home/impl/src/main/res/values/localazy.xml
+++ b/features/home/impl/src/main/res/values/localazy.xml
@@ -33,6 +33,7 @@ For now, you can deselect filters in order to see your other chats"
"Invites"
"You don\'t have any pending invites."
"Low Priority"
+ "You don’t have any low priority chats yet"
"You can deselect filters in order to see your other chats"
"You don’t have chats for this selection"
"People"
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt
index e29b2cf580..4fcbcd170b 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt
@@ -284,7 +284,7 @@ private fun AndroidComposeTestRule.setRoomL
onSettingsClick = onSettingsClick,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
- onCreateRoomClick = onCreateRoomClick,
+ onStartChatClick = onCreateRoomClick,
onRoomSettingsClick = onRoomSettingsClick,
onMenuActionClick = onMenuActionClick,
onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser,
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 f99d5691d9..49a545c4c9 100644
--- a/features/invite/impl/src/main/res/values-eu/translations.xml
+++ b/features/invite/impl/src/main/res/values-eu/translations.xml
@@ -1,10 +1,13 @@
"Blokeatu erabiltzailea"
+ "Eman ezetza eta blokeatu"
"Ziur %1$s(e)ra batzeko gonbidapena baztertu nahi duzula?"
"Baztertu gonbidapena"
"Ziur %1$s(r)en txat pribatua baztertu nahi duzula?"
"Baztertu txata"
"Ez dago gonbidapenik"
"%1$s(e)k (%2$s) gonbidatu zaitu"
+ "Eman gonbidapenari ezetza eta blokeatu"
+ "Eman ezetza eta blokeatu"
diff --git a/features/invite/impl/src/main/res/values-zh/translations.xml b/features/invite/impl/src/main/res/values-zh/translations.xml
index a340e22984..e7cf39a9c3 100644
--- a/features/invite/impl/src/main/res/values-zh/translations.xml
+++ b/features/invite/impl/src/main/res/values-zh/translations.xml
@@ -1,10 +1,18 @@
+ "您不会看到来自该用户的任何信息或房间邀请"
"封禁用户"
+ "向您的帐户提供商举报此房间。"
+ "描述举报的原因…"
+ "拒绝并屏蔽"
"您确定要拒绝加入 %1$s 的邀请吗?"
"拒绝邀请"
"您确定要拒绝与 %1$s 开始私聊吗?"
"拒绝聊天"
"没有邀请"
"%1$s (%2$s)邀请了你"
+ "是的,拒绝并屏蔽"
+ "您确定要拒绝加入此房间的邀请吗?这也将阻止%1$s 与您联系或邀请您加入房间。"
+ "拒绝邀请并屏蔽"
+ "拒绝并屏蔽"
diff --git a/features/invitepeople/api/build.gradle.kts b/features/invitepeople/api/build.gradle.kts
new file mode 100644
index 0000000000..3cbd83724a
--- /dev/null
+++ b/features/invitepeople/api/build.gradle.kts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+plugins {
+ id("io.element.android-compose-library")
+}
+
+android {
+ namespace = "io.element.android.features.invitepeople.api"
+}
+
+dependencies {
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.matrix.api)
+}
diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt
new file mode 100644
index 0000000000..0ab097462b
--- /dev/null
+++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.api
+
+interface InvitePeopleEvents {
+ data object SendInvites : InvitePeopleEvents
+ data object CloseSearch : InvitePeopleEvents
+}
diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeoplePresenter.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeoplePresenter.kt
new file mode 100644
index 0000000000..46903bfb18
--- /dev/null
+++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeoplePresenter.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.invitepeople.api
+
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.room.JoinedRoom
+
+interface InvitePeoplePresenter : Presenter {
+ interface Factory {
+ fun create(
+ joinedRoom: JoinedRoom?,
+ roomId: RoomId,
+ ): InvitePeoplePresenter
+ }
+}
diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleRenderer.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleRenderer.kt
new file mode 100644
index 0000000000..30144a11d1
--- /dev/null
+++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleRenderer.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.invitepeople.api
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+interface InvitePeopleRenderer {
+ @Composable
+ fun Render(
+ state: InvitePeopleState,
+ modifier: Modifier,
+ )
+}
diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt
new file mode 100644
index 0000000000..db8b9ffbd2
--- /dev/null
+++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.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.invitepeople.api
+
+interface InvitePeopleState {
+ val canInvite: Boolean
+ val isSearchActive: Boolean
+ val eventSink: (InvitePeopleEvents) -> Unit
+}
diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt
new file mode 100644
index 0000000000..b69ad7c225
--- /dev/null
+++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.api
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+
+class InvitePeopleStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aPreviewInvitePeopleState(),
+ aPreviewInvitePeopleState(canInvite = true),
+ aPreviewInvitePeopleState(isSearchActive = true)
+ )
+}
+
+private data class PreviewInvitePeopleState(
+ override val canInvite: Boolean,
+ override val isSearchActive: Boolean,
+ override val eventSink: (InvitePeopleEvents) -> Unit,
+) : InvitePeopleState
+
+private fun aPreviewInvitePeopleState(
+ canInvite: Boolean = false,
+ isSearchActive: Boolean = false,
+ eventSink: (InvitePeopleEvents) -> Unit = {},
+) = PreviewInvitePeopleState(
+ canInvite = canInvite,
+ isSearchActive = isSearchActive,
+ eventSink = eventSink
+)
diff --git a/features/invitepeople/impl/build.gradle.kts b/features/invitepeople/impl/build.gradle.kts
new file mode 100644
index 0000000000..bdb1c6942e
--- /dev/null
+++ b/features/invitepeople/impl/build.gradle.kts
@@ -0,0 +1,53 @@
+import extension.setupAnvil
+
+/*
+ * Copyright 2022-2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+plugins {
+ id("io.element.android-compose-library")
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = "io.element.android.features.invitepeople.impl"
+
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+}
+
+setupAnvil()
+
+dependencies {
+ implementation(projects.libraries.core)
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.matrix.api)
+ implementation(projects.libraries.matrixui)
+ implementation(projects.libraries.designsystem)
+ implementation(projects.libraries.uiStrings)
+ implementation(projects.libraries.androidutils)
+ implementation(projects.libraries.usersearch.impl)
+ implementation(libs.coil.compose)
+ implementation(projects.services.apperror.api)
+ api(projects.features.invitepeople.api)
+
+ testImplementation(libs.test.junit)
+ testImplementation(libs.test.mockk)
+ testImplementation(libs.coroutines.test)
+ testImplementation(libs.molecule.runtime)
+ testImplementation(libs.test.truth)
+ testImplementation(libs.test.turbine)
+ testImplementation(libs.test.robolectric)
+ testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.libraries.usersearch.test)
+ testImplementation(projects.services.apperror.test)
+ testImplementation(projects.tests.testutils)
+ testImplementation(libs.androidx.compose.ui.test.junit)
+ testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
+}
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt
new file mode 100644
index 0000000000..984eefbc77
--- /dev/null
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.impl
+
+import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.libraries.matrix.api.user.MatrixUser
+
+sealed interface DefaultInvitePeopleEvents : InvitePeopleEvents {
+ data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents
+ data class UpdateSearchQuery(val query: String) : DefaultInvitePeopleEvents
+ data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt
similarity index 57%
rename from features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt
rename to features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt
index ed5d13f70b..8961e1157f 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt
@@ -5,50 +5,88 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.roomdetails.impl.invite
+package io.element.android.features.invitepeople.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
-import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
+import com.squareup.anvil.annotations.ContributesBinding
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.features.invitepeople.api.InvitePeoplePresenter
+import io.element.android.features.invitepeople.api.InvitePeopleState
import io.element.android.libraries.architecture.AsyncData
-import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.architecture.map
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
+import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
+import io.element.android.libraries.matrix.api.room.filterMembers
import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.usersearch.api.UserRepository
+import io.element.android.services.apperror.api.AppErrorStateService
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import javax.inject.Inject
-class RoomInviteMembersPresenter @Inject constructor(
+class DefaultInvitePeoplePresenter @AssistedInject constructor(
+ @Assisted private val joinedRoom: JoinedRoom?,
+ @Assisted private val roomId: RoomId,
private val userRepository: UserRepository,
- private val roomMemberListDataSource: RoomMemberListDataSource,
private val coroutineDispatchers: CoroutineDispatchers,
-) : Presenter {
+ @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
+ private val appErrorStateService: AppErrorStateService,
+ private val matrixClient: MatrixClient,
+) : InvitePeoplePresenter {
+ @AssistedFactory
+ @ContributesBinding(SessionScope::class)
+ interface Factory : InvitePeoplePresenter.Factory {
+ override fun create(joinedRoom: JoinedRoom?, roomId: RoomId): DefaultInvitePeoplePresenter
+ }
+
@Composable
- override fun present(): RoomInviteMembersState {
+ override fun present(): InvitePeopleState {
val roomMembers = remember { mutableStateOf>>(AsyncData.Loading()) }
val selectedUsers = remember { mutableStateOf>(persistentListOf()) }
val searchResults = remember { mutableStateOf>>(SearchBarResultState.Initial()) }
var searchQuery by rememberSaveable { mutableStateOf("") }
var searchActive by rememberSaveable { mutableStateOf(false) }
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
+ val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) {
+ if (joinedRoom == null) {
+ val result = matrixClient.getJoinedRoom(roomId)
+ value = if (result == null) {
+ AsyncData.Failure(Exception("Room not found"))
+ } else {
+ AsyncData.Success(result)
+ }
+ }
+ }
- LaunchedEffect(Unit) {
- fetchMembers(roomMembers)
+ LaunchedEffect(room.isSuccess()) {
+ room.dataOrNull()?.let {
+ fetchMembers(it, roomMembers)
+ }
}
LaunchedEffect(searchQuery, roomMembers) {
performSearch(
@@ -60,33 +98,61 @@ class RoomInviteMembersPresenter @Inject constructor(
)
}
- return RoomInviteMembersState(
+ fun handleEvents(event: InvitePeopleEvents) {
+ when (event) {
+ is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
+ searchActive = event.active
+ searchQuery = ""
+ }
+
+ is DefaultInvitePeopleEvents.UpdateSearchQuery -> {
+ searchQuery = event.query
+ }
+
+ is DefaultInvitePeopleEvents.ToggleUser -> {
+ selectedUsers.toggleUser(event.user)
+ searchResults.toggleUser(event.user)
+ }
+ is InvitePeopleEvents.SendInvites -> {
+ room.dataOrNull()?.let {
+ sessionCoroutineScope.sendInvites(it, selectedUsers.value)
+ }
+ }
+ is InvitePeopleEvents.CloseSearch -> {
+ searchActive = false
+ searchQuery = ""
+ }
+ }
+ }
+
+ return DefaultInvitePeopleState(
+ room = room.map { },
canInvite = selectedUsers.value.isNotEmpty(),
selectedUsers = selectedUsers.value,
searchQuery = searchQuery,
isSearchActive = searchActive,
searchResults = searchResults.value,
showSearchLoader = showSearchLoader.value,
- eventSink = {
- when (it) {
- is RoomInviteMembersEvents.OnSearchActiveChanged -> {
- searchActive = it.active
- searchQuery = ""
- }
-
- is RoomInviteMembersEvents.UpdateSearchQuery -> {
- searchQuery = it.query
- }
-
- is RoomInviteMembersEvents.ToggleUser -> {
- selectedUsers.toggleUser(it.user)
- searchResults.toggleUser(it.user)
- }
- }
- }
+ eventSink = ::handleEvents,
)
}
+ private fun CoroutineScope.sendInvites(
+ room: JoinedRoom,
+ selectedUsers: List,
+ ) = launch {
+ val anyInviteFailed = selectedUsers
+ .map { room.inviteUserById(it.userId) }
+ .any { it.isFailure }
+
+ if (anyInviteFailed) {
+ appErrorStateService.showError(
+ titleRes = CommonStrings.common_unable_to_invite_title,
+ bodyRes = CommonStrings.common_unable_to_invite_message,
+ )
+ }
+ }
+
@JvmName("toggleUserInSelectedUsers")
private fun MutableState>.toggleUser(user: MatrixUser) {
value = if (value.contains(user)) {
@@ -134,7 +200,7 @@ class RoomInviteMembersPresenter @Inject constructor(
val isInvited = existingMembership == RoomMembershipState.INVITE
InvitableUser(
matrixUser = result.matrixUser,
- isSelected = selectedUsers.value.contains(result.matrixUser) || isJoined || isInvited,
+ isSelected = selectedUsers.value.contains(result.matrixUser),
isAlreadyJoined = isJoined,
isAlreadyInvited = isInvited,
isUnresolved = result.isUnresolved,
@@ -144,11 +210,12 @@ class RoomInviteMembersPresenter @Inject constructor(
}.launchIn(this)
}
- private suspend fun fetchMembers(roomMembers: MutableState>>) {
+ private suspend fun fetchMembers(
+ room: JoinedRoom,
+ roomMembers: MutableState>>
+ ) {
suspend {
- withContext(coroutineDispatchers.io) {
- roomMemberListDataSource.search("").toImmutableList()
- }
+ room.filterMembers("", coroutineDispatchers.io).toImmutableList()
}.runCatchingUpdatingState(roomMembers)
}
}
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleRenderer.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleRenderer.kt
new file mode 100644
index 0000000000..8207e75fd5
--- /dev/null
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleRenderer.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.invitepeople.impl
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.invitepeople.api.InvitePeopleRenderer
+import io.element.android.features.invitepeople.api.InvitePeopleState
+import io.element.android.libraries.di.SessionScope
+import javax.inject.Inject
+
+@ContributesBinding(SessionScope::class)
+class DefaultInvitePeopleRenderer @Inject constructor() : InvitePeopleRenderer {
+ @Composable
+ override fun Render(state: InvitePeopleState, modifier: Modifier) {
+ if (state is DefaultInvitePeopleState) {
+ InvitePeopleView(
+ state = state,
+ modifier = modifier
+ )
+ } else {
+ error("Unsupported state type: ${state::javaClass}")
+ }
+ }
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt
similarity index 51%
rename from features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt
rename to features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt
index c7927247ef..77ba8aad05 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt
@@ -1,30 +1,26 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.roomdetails.impl.invite
+package io.element.android.features.invitepeople.impl
+import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.features.invitepeople.api.InvitePeopleState
+import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
-data class RoomInviteMembersState(
- val canInvite: Boolean,
+data class DefaultInvitePeopleState(
+ val room: AsyncData,
+ override val canInvite: Boolean,
val searchQuery: String,
val showSearchLoader: Boolean,
val searchResults: SearchBarResultState>,
val selectedUsers: ImmutableList,
- val isSearchActive: Boolean,
- val eventSink: (RoomInviteMembersEvents) -> Unit,
-)
-
-data class InvitableUser(
- val matrixUser: MatrixUser,
- val isSelected: Boolean = false,
- val isAlreadyJoined: Boolean = false,
- val isAlreadyInvited: Boolean = false,
- val isUnresolved: Boolean = false,
-)
+ override val isSearchActive: Boolean,
+ override val eventSink: (InvitePeopleEvents) -> Unit
+) : InvitePeopleState
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt
similarity index 50%
rename from features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt
rename to features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt
index 7675880f37..980d32e7fb 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt
@@ -5,9 +5,10 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.roomdetails.impl.invite
+package io.element.android.features.invitepeople.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUser
@@ -16,15 +17,15 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
-internal class RoomInviteMembersStateProvider : PreviewParameterProvider {
- override val values: Sequence
+internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider {
+ override val values: Sequence
get() = sequenceOf(
- aRoomInviteMembersState(),
- aRoomInviteMembersState(canInvite = true, selectedUsers = aMatrixUserList().toImmutableList()),
- aRoomInviteMembersState(isSearchActive = true, searchQuery = "some query"),
- aRoomInviteMembersState(isSearchActive = true, searchQuery = "some query", selectedUsers = aMatrixUserList().toImmutableList()),
- aRoomInviteMembersState(isSearchActive = true, searchQuery = "some query", searchResults = SearchBarResultState.NoResultsFound()),
- aRoomInviteMembersState(
+ aDefaultInvitePeopleState(),
+ aDefaultInvitePeopleState(canInvite = true, selectedUsers = aMatrixUserList().toImmutableList()),
+ aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query"),
+ aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query", selectedUsers = aMatrixUserList().toImmutableList()),
+ aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query", searchResults = SearchBarResultState.NoResultsFound()),
+ aDefaultInvitePeopleState(
isSearchActive = true,
canInvite = true,
searchQuery = "some query",
@@ -33,15 +34,15 @@ internal class RoomInviteMembersStateProvider : PreviewParameterProvider = AsyncData.Success(Unit),
canInvite: Boolean = false,
searchQuery: String = "",
searchResults: SearchBarResultState> = SearchBarResultState.Initial(),
selectedUsers: ImmutableList = persistentListOf(),
isSearchActive: Boolean = false,
showSearchLoader: Boolean = false,
-): RoomInviteMembersState {
- return RoomInviteMembersState(
+): DefaultInvitePeopleState {
+ return DefaultInvitePeopleState(
+ room = room,
canInvite = canInvite,
searchQuery = searchQuery,
searchResults = searchResults,
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitableUser.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitableUser.kt
new file mode 100644
index 0000000000..7dfd6a0f36
--- /dev/null
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitableUser.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.invitepeople.impl
+
+import io.element.android.libraries.matrix.api.user.MatrixUser
+
+data class InvitableUser(
+ val matrixUser: MatrixUser,
+ val isSelected: Boolean,
+ val isAlreadyJoined: Boolean,
+ val isAlreadyInvited: Boolean,
+ val isUnresolved: Boolean,
+)
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt
new file mode 100644
index 0000000000..5363ca43f1
--- /dev/null
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.impl
+
+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.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.designsystem.components.async.AsyncFailure
+import io.element.android.libraries.designsystem.components.async.AsyncLoading
+import io.element.android.libraries.designsystem.components.avatar.AvatarSize
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.SearchBar
+import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.matrix.ui.components.CheckableUserRow
+import io.element.android.libraries.matrix.ui.components.CheckableUserRowData
+import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
+import io.element.android.libraries.matrix.ui.model.getAvatarData
+import io.element.android.libraries.matrix.ui.model.getBestName
+import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.ImmutableList
+
+@Composable
+fun InvitePeopleView(
+ state: DefaultInvitePeopleState,
+ modifier: Modifier = Modifier,
+) {
+ when (state.room) {
+ is AsyncData.Failure -> InvitePeopleViewError(state.room.error, modifier)
+ AsyncData.Uninitialized,
+ is AsyncData.Loading,
+ is AsyncData.Success -> InvitePeopleContentView(state, modifier)
+ }
+}
+
+@Composable
+private fun InvitePeopleViewError(
+ error: Throwable,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ AsyncFailure(
+ throwable = error,
+ onRetry = null,
+ modifier = Modifier.padding(horizontal = 16.dp),
+ )
+ }
+}
+
+@Composable
+private fun InvitePeopleContentView(
+ state: DefaultInvitePeopleState,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ InvitePeopleSearchBar(
+ modifier = Modifier.fillMaxWidth(),
+ query = state.searchQuery,
+ showLoader = state.showSearchLoader,
+ selectedUsers = state.selectedUsers,
+ state = state.searchResults,
+ active = state.isSearchActive,
+ onActiveChange = {
+ state.eventSink(
+ DefaultInvitePeopleEvents.OnSearchActiveChanged(
+ it
+ )
+ )
+ },
+ onTextChange = { state.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery(it)) },
+ onToggleUser = { state.eventSink(DefaultInvitePeopleEvents.ToggleUser(it)) },
+ )
+
+ if (!state.isSearchActive) {
+ SelectedUsersRowList(
+ modifier = Modifier.fillMaxWidth(),
+ selectedUsers = state.selectedUsers,
+ autoScroll = true,
+ onUserRemove = { state.eventSink(DefaultInvitePeopleEvents.ToggleUser(it)) },
+ contentPadding = PaddingValues(16.dp),
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun InvitePeopleSearchBar(
+ query: String,
+ state: SearchBarResultState>,
+ showLoader: Boolean,
+ selectedUsers: ImmutableList,
+ active: Boolean,
+ onActiveChange: (Boolean) -> Unit,
+ onTextChange: (String) -> Unit,
+ onToggleUser: (MatrixUser) -> Unit,
+ modifier: Modifier = Modifier,
+ placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone),
+) {
+ SearchBar(
+ query = query,
+ onQueryChange = onTextChange,
+ active = active,
+ onActiveChange = onActiveChange,
+ modifier = modifier,
+ placeHolderTitle = placeHolderTitle,
+ contentPrefix = {
+ if (selectedUsers.isNotEmpty()) {
+ SelectedUsersRowList(
+ modifier = Modifier.fillMaxWidth(),
+ selectedUsers = selectedUsers,
+ autoScroll = true,
+ onUserRemove = onToggleUser,
+ contentPadding = PaddingValues(16.dp),
+ )
+ }
+ },
+ showBackButton = false,
+ resultState = state,
+ contentSuffix = {
+ if (showLoader) {
+ AsyncLoading()
+ }
+ },
+ resultHandler = { results ->
+ Text(
+ text = stringResource(id = CommonStrings.common_search_results),
+ style = ElementTheme.typography.fontBodyLgMedium,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 8.dp)
+ )
+
+ LazyColumn {
+ itemsIndexed(results) { index, invitableUser ->
+ val invitedOrJoined = invitableUser.isAlreadyInvited || invitableUser.isAlreadyJoined
+ val isUnresolved = invitableUser.isUnresolved && !invitedOrJoined
+ val enabled = isUnresolved || !invitedOrJoined
+ val data = if (isUnresolved) {
+ CheckableUserRowData.Unresolved(
+ avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem),
+ id = invitableUser.matrixUser.userId.value,
+ )
+ } else {
+ CheckableUserRowData.Resolved(
+ avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem),
+ name = invitableUser.matrixUser.getBestName(),
+ subtext = when {
+ // If they're already invited or joined we show that information
+ invitableUser.isAlreadyJoined -> stringResource(R.string.screen_invite_users_already_a_member)
+ invitableUser.isAlreadyInvited -> stringResource(R.string.screen_invite_users_already_invited)
+ // Otherwise show the ID, unless that's already used for their name
+ invitableUser.matrixUser.displayName.isNullOrEmpty()
+ .not() -> invitableUser.matrixUser.userId.value
+ else -> null
+ }
+ )
+ }
+ CheckableUserRow(
+ checked = invitableUser.isSelected || invitedOrJoined,
+ enabled = enabled,
+ data = data,
+ onCheckedChange = { onToggleUser(invitableUser.matrixUser) },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ },
+ )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun InvitePeopleViewPreview(@PreviewParameter(DefaultInvitePeopleStateProvider::class) state: DefaultInvitePeopleState) =
+ ElementPreview {
+ InvitePeopleView(state = state)
+ }
diff --git a/features/invitepeople/impl/src/main/res/values-be/translations.xml b/features/invitepeople/impl/src/main/res/values-be/translations.xml
new file mode 100644
index 0000000000..7125d8b2e6
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-be/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Ужо ўдзельнік"
+ "Ужо запрасілі"
+
diff --git a/features/invitepeople/impl/src/main/res/values-bg/translations.xml b/features/invitepeople/impl/src/main/res/values-bg/translations.xml
new file mode 100644
index 0000000000..f585980920
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-bg/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Вече е член"
+ "Вече е бил поканен"
+
diff --git a/features/invitepeople/impl/src/main/res/values-cs/translations.xml b/features/invitepeople/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..fa5b3aa9a9
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Již členem"
+ "Již pozván(a)"
+
diff --git a/features/invitepeople/impl/src/main/res/values-cy/translations.xml b/features/invitepeople/impl/src/main/res/values-cy/translations.xml
new file mode 100644
index 0000000000..c58df3c541
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-cy/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Eisoes yn aelod"
+ "Wedi gwahodd yn barod"
+
diff --git a/features/invitepeople/impl/src/main/res/values-da/translations.xml b/features/invitepeople/impl/src/main/res/values-da/translations.xml
new file mode 100644
index 0000000000..fbb1814e9f
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-da/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Allerede medlem"
+ "Allerede inviteret"
+
diff --git a/features/invitepeople/impl/src/main/res/values-de/translations.xml b/features/invitepeople/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..182d9f289a
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Bereits Mitglied"
+ "Bereits eingeladen"
+
diff --git a/features/invitepeople/impl/src/main/res/values-el/translations.xml b/features/invitepeople/impl/src/main/res/values-el/translations.xml
new file mode 100644
index 0000000000..3e3c1fd5d2
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-el/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Ήδη μέλος"
+ "Ήδη προσκεκλημένος"
+
diff --git a/features/invitepeople/impl/src/main/res/values-es/translations.xml b/features/invitepeople/impl/src/main/res/values-es/translations.xml
new file mode 100644
index 0000000000..e62bb211e5
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-es/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Ya eres miembro"
+ "Ya estás invitado"
+
diff --git a/features/invitepeople/impl/src/main/res/values-et/translations.xml b/features/invitepeople/impl/src/main/res/values-et/translations.xml
new file mode 100644
index 0000000000..44484d23c3
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-et/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Sa juba oled jututoa liige"
+ "Sa juba oled kutse saanud"
+
diff --git a/features/invitepeople/impl/src/main/res/values-eu/translations.xml b/features/invitepeople/impl/src/main/res/values-eu/translations.xml
new file mode 100644
index 0000000000..a6aedd89cd
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-eu/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Kidea da dagoeneko"
+ "Lehendik ere gonbidatuta"
+
diff --git a/features/invitepeople/impl/src/main/res/values-fa/translations.xml b/features/invitepeople/impl/src/main/res/values-fa/translations.xml
new file mode 100644
index 0000000000..544f01b8c8
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-fa/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "از پیش عضو است"
+ "از پیش دعوت شده"
+
diff --git a/features/invitepeople/impl/src/main/res/values-fi/translations.xml b/features/invitepeople/impl/src/main/res/values-fi/translations.xml
new file mode 100644
index 0000000000..e347919719
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-fi/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "On jo jäsen"
+ "On jo kutsuttu"
+
diff --git a/features/invitepeople/impl/src/main/res/values-fr/translations.xml b/features/invitepeople/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..dcc16f58cf
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Déjà membre"
+ "Déjà invité(e)"
+
diff --git a/features/invitepeople/impl/src/main/res/values-hu/translations.xml b/features/invitepeople/impl/src/main/res/values-hu/translations.xml
new file mode 100644
index 0000000000..16f35b018c
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-hu/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Már tag"
+ "Már meghívták"
+
diff --git a/features/invitepeople/impl/src/main/res/values-in/translations.xml b/features/invitepeople/impl/src/main/res/values-in/translations.xml
new file mode 100644
index 0000000000..e112033a06
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-in/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Sudah menjadi anggota"
+ "Sudah diundang"
+
diff --git a/features/invitepeople/impl/src/main/res/values-it/translations.xml b/features/invitepeople/impl/src/main/res/values-it/translations.xml
new file mode 100644
index 0000000000..979e42de1b
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-it/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Già membro"
+ "Già invitato"
+
diff --git a/features/invitepeople/impl/src/main/res/values-ka/translations.xml b/features/invitepeople/impl/src/main/res/values-ka/translations.xml
new file mode 100644
index 0000000000..3a34c2edac
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-ka/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "უკვე წევრია"
+ "უკვე მოწვეულია"
+
diff --git a/features/invitepeople/impl/src/main/res/values-lt/translations.xml b/features/invitepeople/impl/src/main/res/values-lt/translations.xml
new file mode 100644
index 0000000000..59c15290a1
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-lt/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Jau narys"
+ "Jau pakviestas"
+
diff --git a/features/invitepeople/impl/src/main/res/values-nb/translations.xml b/features/invitepeople/impl/src/main/res/values-nb/translations.xml
new file mode 100644
index 0000000000..617b9271d5
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-nb/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Allerede medlem"
+ "Allerede invitert"
+
diff --git a/features/invitepeople/impl/src/main/res/values-nl/translations.xml b/features/invitepeople/impl/src/main/res/values-nl/translations.xml
new file mode 100644
index 0000000000..b978dcd9cc
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-nl/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Reeds lid"
+ "Reeds uitgenodigd"
+
diff --git a/features/invitepeople/impl/src/main/res/values-pl/translations.xml b/features/invitepeople/impl/src/main/res/values-pl/translations.xml
new file mode 100644
index 0000000000..bfd537bb4b
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-pl/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Jest już członkiem"
+ "Już zaproszony"
+
diff --git a/features/invitepeople/impl/src/main/res/values-pt-rBR/translations.xml b/features/invitepeople/impl/src/main/res/values-pt-rBR/translations.xml
new file mode 100644
index 0000000000..7ad049843b
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-pt-rBR/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Já é membro"
+ "Já foi convidado"
+
diff --git a/features/invitepeople/impl/src/main/res/values-pt/translations.xml b/features/invitepeople/impl/src/main/res/values-pt/translations.xml
new file mode 100644
index 0000000000..a953d11f67
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-pt/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Já é participante"
+ "Já foi convidado"
+
diff --git a/features/invitepeople/impl/src/main/res/values-ro/translations.xml b/features/invitepeople/impl/src/main/res/values-ro/translations.xml
new file mode 100644
index 0000000000..f03be4b263
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-ro/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Deja membru"
+ "Deja invitat"
+
diff --git a/features/invitepeople/impl/src/main/res/values-ru/translations.xml b/features/invitepeople/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..0ae8eb792c
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Уже зарегистрирован"
+ "Уже приглашены"
+
diff --git a/features/invitepeople/impl/src/main/res/values-sk/translations.xml b/features/invitepeople/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..68e34b0ada
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Už ste členom"
+ "Už ste pozvaní"
+
diff --git a/features/invitepeople/impl/src/main/res/values-sv/translations.xml b/features/invitepeople/impl/src/main/res/values-sv/translations.xml
new file mode 100644
index 0000000000..fd670c9c1a
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-sv/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Redan medlem"
+ "Redan inbjuden"
+
diff --git a/features/invitepeople/impl/src/main/res/values-tr/translations.xml b/features/invitepeople/impl/src/main/res/values-tr/translations.xml
new file mode 100644
index 0000000000..427a7f3f10
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-tr/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Zaten üye"
+ "Zaten davet edildi"
+
diff --git a/features/invitepeople/impl/src/main/res/values-uk/translations.xml b/features/invitepeople/impl/src/main/res/values-uk/translations.xml
new file mode 100644
index 0000000000..da3ac9fe5b
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-uk/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Уже учасник"
+ "Уже запрошені"
+
diff --git a/features/invitepeople/impl/src/main/res/values-ur/translations.xml b/features/invitepeople/impl/src/main/res/values-ur/translations.xml
new file mode 100644
index 0000000000..06e8cee414
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-ur/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "پہلے سے ہی رکن"
+ "پہلے سے مدعو شدہ"
+
diff --git a/features/invitepeople/impl/src/main/res/values-uz/translations.xml b/features/invitepeople/impl/src/main/res/values-uz/translations.xml
new file mode 100644
index 0000000000..445a62d318
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-uz/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Allaqachon a\'zo"
+ "Allaqachon taklif qilingan"
+
diff --git a/features/invitepeople/impl/src/main/res/values-zh-rTW/translations.xml b/features/invitepeople/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..c8117ec283
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "已是成員"
+ "已邀請"
+
diff --git a/features/invitepeople/impl/src/main/res/values-zh/translations.xml b/features/invitepeople/impl/src/main/res/values-zh/translations.xml
new file mode 100644
index 0000000000..b1e0e953f8
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-zh/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "已经是成员"
+ "已邀请"
+
diff --git a/features/invitepeople/impl/src/main/res/values/localazy.xml b/features/invitepeople/impl/src/main/res/values/localazy.xml
new file mode 100644
index 0000000000..d89ae92e75
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values/localazy.xml
@@ -0,0 +1,5 @@
+
+
+ "Already a member"
+ "Already invited"
+
diff --git a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt
new file mode 100644
index 0000000000..1e438fab8e
--- /dev/null
+++ b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt
@@ -0,0 +1,547 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.impl
+
+import app.cash.turbine.ReceiveTurbine
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
+import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.room.JoinedRoom
+import io.element.android.libraries.matrix.api.room.RoomMembersState
+import io.element.android.libraries.matrix.api.room.RoomMembershipState
+import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
+import io.element.android.libraries.matrix.test.A_ROOM_ID
+import io.element.android.libraries.matrix.test.A_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.room.FakeJoinedRoom
+import io.element.android.libraries.matrix.test.room.aRoomMember
+import io.element.android.libraries.matrix.test.room.aRoomMemberList
+import io.element.android.libraries.matrix.ui.components.aMatrixUser
+import io.element.android.libraries.matrix.ui.components.aMatrixUserList
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.libraries.usersearch.api.UserRepository
+import io.element.android.libraries.usersearch.api.UserSearchResult
+import io.element.android.libraries.usersearch.api.UserSearchResultState
+import io.element.android.libraries.usersearch.test.FakeUserRepository
+import io.element.android.services.apperror.api.AppErrorStateService
+import io.element.android.services.apperror.test.FakeAppErrorStateService
+import io.element.android.tests.testutils.WarmUpRule
+import io.element.android.tests.testutils.lambda.lambdaError
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
+import io.element.android.tests.testutils.test
+import io.element.android.tests.testutils.testCoroutineDispatchers
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+
+internal class DefaultInvitePeoplePresenterTest {
+ @get:Rule
+ val warmUpRule = WarmUpRule()
+
+ @Test
+ fun `present - initial state has no results and no search`() = runTest {
+ val presenter = createDefaultInvitePeoplePresenter()
+ presenter.test {
+ val initialState = awaitItemAsDefault()
+ assertThat(initialState.room).isEqualTo(AsyncData.Success(Unit))
+ assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
+ assertThat(initialState.isSearchActive).isFalse()
+ assertThat(initialState.canInvite).isFalse()
+ assertThat(initialState.searchQuery).isEmpty()
+
+ skipItems(1)
+ }
+ }
+
+ @Test
+ fun `present - updates search active state`() = runTest {
+ val presenter = createDefaultInvitePeoplePresenter()
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ initialState.eventSink(DefaultInvitePeopleEvents.OnSearchActiveChanged(true))
+
+ val resultState = awaitItem()
+ assertThat(resultState.isSearchActive).isTrue()
+ resultState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
+ assertThat(awaitItemAsDefault().searchQuery).isEqualTo("some query")
+ resultState.eventSink(InvitePeopleEvents.CloseSearch)
+ skipItems(1)
+ awaitItemAsDefault().also {
+ assertThat(it.isSearchActive).isFalse()
+ assertThat(it.searchQuery).isEmpty()
+ }
+ }
+ }
+
+ @Test
+ fun `present - performs search and handles empty result list`() = runTest {
+ val repository = FakeUserRepository()
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
+ assertThat(repository.providedQuery).isEqualTo("some query")
+ repository.emitState(UserSearchResultState(results = emptyList(), isSearching = true))
+ skipItems(3)
+ awaitItemAsDefault().also { state ->
+ assertThat(state.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
+ assertThat(state.showSearchLoader).isTrue()
+ }
+ repository.emitState(results = emptyList(), isSearching = false)
+ awaitItemAsDefault().also { state ->
+ assertThat(state.searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
+ assertThat(state.showSearchLoader).isFalse()
+ }
+ }
+ }
+
+ @Test
+ fun `present - performs search and handles user results`() = runTest {
+ val repository = FakeUserRepository()
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
+ skipItems(1)
+
+ assertThat(repository.providedQuery).isEqualTo("some query")
+ repository.emitStateWithUsers(users = aMatrixUserList())
+ skipItems(1)
+
+ val resultState = awaitItemAsDefault()
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+
+ val expectedUsers = aMatrixUserList()
+ val users = resultState.searchResults.users()
+ expectedUsers.forEachIndexed { index, matrixUser ->
+ assertThat(users[index].matrixUser).isEqualTo(matrixUser)
+ // All users are joined or invited
+ if (users[index].isAlreadyInvited) {
+ assertThat(users[index].isAlreadyJoined).isFalse()
+ } else {
+ assertThat(users[index].isAlreadyJoined).isTrue()
+ }
+ assertThat(users[index].isSelected).isFalse()
+ }
+ }
+ }
+
+ @Test
+ fun `present - performs search and handles membership state of existing users`() = runTest {
+ val userList = aMatrixUserList()
+ val joinedUser = userList[0]
+ val invitedUser = userList[1]
+
+ val repository = FakeUserRepository()
+ val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ roomMembersState = RoomMembersState.Ready(
+ persistentListOf(
+ aRoomMember(
+ userId = joinedUser.userId,
+ membership = RoomMembershipState.JOIN
+ ),
+ aRoomMember(
+ userId = invitedUser.userId,
+ membership = RoomMembershipState.INVITE
+ ),
+ )
+ ),
+ coroutineDispatchers = coroutineDispatchers,
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
+ skipItems(1)
+
+ assertThat(repository.providedQuery).isEqualTo("some query")
+ repository.emitStateWithUsers(users = aMatrixUserList())
+ skipItems(1)
+
+ val resultState = awaitItemAsDefault()
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+
+ val users = resultState.searchResults.users()
+
+ // The result that matches a user with JOINED membership is marked as such
+ val userWhoShouldBeJoined = users.find { it.matrixUser == joinedUser }
+ assertThat(userWhoShouldBeJoined).isNotNull()
+ assertThat(userWhoShouldBeJoined?.isAlreadyJoined).isTrue()
+ assertThat(userWhoShouldBeJoined?.isAlreadyInvited).isFalse()
+
+ // The result that matches a user with INVITED membership is marked as such
+ val userWhoShouldBeInvited = users.find { it.matrixUser == invitedUser }
+ assertThat(userWhoShouldBeInvited).isNotNull()
+ assertThat(userWhoShouldBeInvited?.isAlreadyJoined).isFalse()
+ assertThat(userWhoShouldBeInvited?.isAlreadyInvited).isTrue()
+
+ // All other users are neither joined nor invited
+ val otherUsers = users.minus(userWhoShouldBeInvited!!).minus(userWhoShouldBeJoined!!)
+ assertThat(otherUsers.none { it.isAlreadyInvited }).isTrue()
+ assertThat(otherUsers.none { it.isAlreadyJoined }).isTrue()
+ }
+ }
+
+ @Test
+ fun `present - performs search and handles unresolved results`() = runTest {
+ val userList = aMatrixUserList()
+ val joinedUser = userList[0]
+ val invitedUser = userList[1]
+
+ val repository = FakeUserRepository()
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ roomMembersState =
+ RoomMembersState.Ready(
+ persistentListOf(
+ aRoomMember(
+ userId = joinedUser.userId,
+ membership = RoomMembershipState.JOIN
+ ),
+ aRoomMember(
+ userId = invitedUser.userId,
+ membership = RoomMembershipState.INVITE
+ ),
+ )
+ ),
+ coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
+ )
+
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
+ skipItems(1)
+
+ assertThat(repository.providedQuery).isEqualTo("some query")
+
+ val unresolvedUser =
+ UserSearchResult(aMatrixUser(id = A_USER_ID.value), isUnresolved = true)
+ repository.emitState(listOf(unresolvedUser) + aMatrixUserList().map {
+ UserSearchResult(
+ it
+ )
+ })
+ skipItems(1)
+
+ val resultState = awaitItemAsDefault()
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+
+ val users = resultState.searchResults.users()
+
+ val userWhoShouldBeUnresolved = users.first()
+ assertThat(userWhoShouldBeUnresolved.isUnresolved).isTrue()
+
+ // All other users are neither joined nor invited
+ val otherUsers = users.minus(userWhoShouldBeUnresolved)
+ assertThat(otherUsers.none { it.isUnresolved }).isTrue()
+ }
+ }
+
+ @Test
+ fun `present - toggle users updates selected user state`() = runTest {
+ val repository = FakeUserRepository()
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ coroutineDispatchers = testCoroutineDispatchers()
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ // When we toggle a user not in the list, they are added
+ initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser()))
+ assertThat(awaitItemAsDefault().selectedUsers).containsExactly(aMatrixUser())
+
+ // Toggling a different user also adds them
+ initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser(id = A_USER_ID_2.value)))
+ assertThat(awaitItemAsDefault().selectedUsers).containsExactly(
+ aMatrixUser(),
+ aMatrixUser(id = A_USER_ID_2.value)
+ )
+
+ // Toggling the first user removes them
+ initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser()))
+ assertThat(awaitItemAsDefault().selectedUsers).containsExactly(aMatrixUser(id = A_USER_ID_2.value))
+ }
+ }
+
+ @Test
+ fun `present - selected users appear as such in search results`() = runTest {
+ val repository = FakeUserRepository()
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ val selectedUser = aMatrixUser()
+
+ initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
+
+ initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
+ skipItems(1)
+
+ assertThat(repository.providedQuery).isEqualTo("some query")
+ repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
+ skipItems(2)
+
+ val resultState = awaitItemAsDefault()
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+
+ val users = resultState.searchResults.users()
+
+ // The one user we have previously toggled is marked as selected
+ val shouldBeSelectedUser = users.find { it.matrixUser == selectedUser }
+ assertThat(shouldBeSelectedUser).isNotNull()
+ assertThat(shouldBeSelectedUser?.isSelected).isTrue()
+
+ // And no others are
+ val allOtherUsers = users.minus(shouldBeSelectedUser!!)
+ assertThat(allOtherUsers.none { it.isSelected }).isTrue()
+ }
+ }
+
+ @Test
+ fun `present - toggling a user updates existing search results`() = runTest {
+ val repository = FakeUserRepository()
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ val selectedUser = aMatrixUser()
+
+ // Given a query is made
+ initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
+ skipItems(1)
+
+ assertThat(repository.providedQuery).isEqualTo("some query")
+ repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
+ skipItems(1)
+ awaitItemAsDefault().also { state ->
+ // selectedUser is not selected
+ assertThat(state.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+ val users = state.searchResults.users()
+ val shouldNotBeSelectedUser = users.find { it.matrixUser == selectedUser }
+ assertThat(shouldNotBeSelectedUser).isNotNull()
+ assertThat(shouldNotBeSelectedUser?.isSelected).isFalse()
+ }
+
+ // And then a user is toggled
+ initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
+ skipItems(1)
+ val resultState = awaitItemAsDefault()
+
+ // The results are updated...
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+ val users = resultState.searchResults.users()
+
+ // The one user we have now toggled is marked as selected
+ val shouldBeSelectedUser = users.find { it.matrixUser == selectedUser }
+ assertThat(shouldBeSelectedUser).isNotNull()
+ assertThat(shouldBeSelectedUser?.isSelected).isTrue()
+
+ // And no others are
+ val allOtherUsers = users.minus(shouldBeSelectedUser!!)
+ assertThat(allOtherUsers.none { it.isSelected }).isTrue()
+ }
+ }
+
+ @Test
+ fun `present - toggling a user and send invite success`() = runTest {
+ val repository = FakeUserRepository()
+ val inviteUserResult = lambdaRecorder> { userId: UserId ->
+ Result.success(Unit)
+ }
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ inviteUserResult = inviteUserResult,
+ coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+ val selectedUser = aMatrixUser()
+ repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
+ skipItems(1)
+ // And then a user is toggled
+ initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
+ skipItems(1)
+ val resultState = awaitItemAsDefault()
+ // The results are updated...
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+ // Send invites
+ initialState.eventSink(InvitePeopleEvents.SendInvites)
+ delay(1_000)
+ inviteUserResult.assertions().isCalledOnce().with(
+ value(selectedUser.userId)
+ )
+ }
+ }
+
+ @Test
+ fun `present - toggling a user and send invite error`() = runTest {
+ val repository = FakeUserRepository()
+ val inviteUserResult = lambdaRecorder> { _: UserId ->
+ Result.failure(AN_EXCEPTION)
+ }
+ val showErrorResResult = lambdaRecorder { _, _ -> }
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ inviteUserResult = inviteUserResult,
+ coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
+ appErrorStateService = FakeAppErrorStateService(
+ showErrorResResult = showErrorResResult,
+ )
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+ val selectedUser = aMatrixUser()
+ repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
+ skipItems(1)
+ // And then a user is toggled
+ initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
+ skipItems(1)
+ val resultState = awaitItemAsDefault()
+ // The results are updated...
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+ // Send invites
+ initialState.eventSink(InvitePeopleEvents.SendInvites)
+ delay(1_000)
+ inviteUserResult.assertions().isCalledOnce().with(
+ value(selectedUser.userId)
+ )
+ showErrorResResult.assertions()
+ .isCalledOnce()
+ .with(
+ value(CommonStrings.common_unable_to_invite_title),
+ value(CommonStrings.common_unable_to_invite_message)
+ )
+ }
+ }
+
+ @Test
+ fun `present - when joinedRoom is not provided, it is retrieved on the MatrixClient`() = runTest {
+ val matrixClient = FakeMatrixClient().apply {
+ givenGetRoomResult(A_ROOM_ID, FakeJoinedRoom())
+ }
+ val presenter = createDefaultInvitePeoplePresenter(
+ joinedRoom = null,
+ roomId = A_ROOM_ID,
+ matrixClient = matrixClient,
+ )
+ presenter.test {
+ val initialState = awaitItemAsDefault()
+ assertThat(initialState.room.isLoading()).isTrue()
+ val finalState = awaitItemAsDefault()
+ assertThat(finalState.room).isEqualTo(AsyncData.Success(Unit))
+ }
+ }
+
+ @Test
+ fun `present - when joinedRoom is not provided, it is retrieved on the MatrixClient - error case`() = runTest {
+ val matrixClient = FakeMatrixClient()
+ val presenter = createDefaultInvitePeoplePresenter(
+ joinedRoom = null,
+ roomId = A_ROOM_ID,
+ matrixClient = matrixClient,
+ )
+ presenter.test {
+ val initialState = awaitItemAsDefault()
+ assertThat(initialState.room.isLoading()).isTrue()
+ val finalState = awaitItemAsDefault()
+ assertThat(finalState.room.errorOrNull()?.message).isEqualTo("Room not found")
+ }
+ }
+
+ private suspend fun FakeUserRepository.emitStateWithUsers(
+ users: List,
+ isSearching: Boolean = false
+ ) {
+ emitState(
+ results = users.map { UserSearchResult(it) },
+ isSearching = isSearching,
+ )
+ }
+
+ private suspend fun FakeUserRepository.emitState(
+ results: List,
+ isSearching: Boolean = false
+ ) {
+ val state = UserSearchResultState(
+ results = results,
+ isSearching = isSearching
+ )
+ emitState(state)
+ }
+
+ private fun SearchBarResultState>.users() =
+ (this as? SearchBarResultState.Results>)?.results.orEmpty()
+}
+
+private suspend fun ReceiveTurbine.awaitItemAsDefault(): DefaultInvitePeopleState {
+ return awaitItem() as DefaultInvitePeopleState
+}
+
+fun TestScope.createDefaultInvitePeoplePresenter(
+ roomMembersState: RoomMembersState = RoomMembersState.Ready(aRoomMemberList()),
+ inviteUserResult: (UserId) -> Result = { lambdaError() },
+ joinedRoom: JoinedRoom? = FakeJoinedRoom(
+ inviteUserResult = inviteUserResult,
+ ).apply {
+ givenRoomMembersState(roomMembersState)
+ },
+ roomId: RoomId = A_ROOM_ID,
+ userRepository: UserRepository = FakeUserRepository(),
+ coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
+ appErrorStateService: AppErrorStateService = FakeAppErrorStateService(),
+ matrixClient: MatrixClient = FakeMatrixClient(),
+): DefaultInvitePeoplePresenter {
+ return DefaultInvitePeoplePresenter(
+ joinedRoom = joinedRoom,
+ roomId = roomId,
+ userRepository = userRepository,
+ coroutineDispatchers = coroutineDispatchers,
+ sessionCoroutineScope = backgroundScope,
+ appErrorStateService = appErrorStateService,
+ matrixClient = matrixClient,
+ )
+}
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt
index cdeac0afe9..d20e1fe596 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
@@ -46,7 +46,7 @@ import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAt
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
-import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMembersCountMolecule
+import io.element.android.libraries.designsystem.atomic.molecules.MembersCountMolecule
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.Announcement
@@ -546,7 +546,7 @@ private fun DefaultLoadedContent(
},
memberCount = {
if (contentState.showMemberCount) {
- RoomPreviewMembersCountMolecule(memberCount = contentState.numberOfMembers ?: 0)
+ MembersCountMolecule(memberCount = contentState.numberOfMembers ?: 0)
}
}
)
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 4d6ddd9dbd..e22c65fcb9 100644
--- a/features/joinroom/impl/src/main/res/values-eu/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-eu/translations.xml
@@ -3,6 +3,8 @@
"Arrazoia: %1$s."
"Utzi eskaera bertan behera"
"Bai, utzi bertan behera"
+ "Eman gonbidapenari ezetza eta blokeatu"
+ "Eman ezetza eta blokeatu"
"Gelara sartzeak huts egin du."
"Ahaztu gela hau"
"Sartu gelan"
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 9e3c6af16f..b9d2da01ad 100644
--- a/features/joinroom/impl/src/main/res/values-fi/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-fi/translations.xml
@@ -18,7 +18,7 @@
"Liity huoneeseen"
"Saatat tarvita kutsun tai olla tilan jäsen, jotta voit liittyä."
"Lähetä liittymispyyntö"
- "Sallitut merkit %1$d / %2$d"
+ "%1$d merkkiä käytetty, %2$d merkkiä sallittu"
"Viesti (valinnainen)"
"Saat kutsun liittyä huoneeseen, jos pyyntösi hyväksytään."
"Liittymispyyntö lähetetty"
diff --git a/features/joinroom/impl/src/main/res/values-sv/translations.xml b/features/joinroom/impl/src/main/res/values-sv/translations.xml
index a6aa79ebd9..78924f1cd6 100644
--- a/features/joinroom/impl/src/main/res/values-sv/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-sv/translations.xml
@@ -18,6 +18,7 @@
"Gå med i rummet"
"Du kan behöva bli inbjuden eller vara medlem i ett utrymme för att gå med."
"Knacka för att gå med"
+ "Tillåtna tecken %1$d av %2$d"
"Meddelande (valfritt)"
"Du kommer att få en inbjudan att gå med i rummet om din begäran accepteras."
"Begäran om att gå med skickad"
diff --git a/features/joinroom/impl/src/main/res/values-zh-rTW/translations.xml b/features/joinroom/impl/src/main/res/values-zh-rTW/translations.xml
index b176a76e0f..a66510111c 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
@@ -18,6 +18,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 8b6e9088c1..50a4c65028 100644
--- a/features/joinroom/impl/src/main/res/values-zh/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-zh/translations.xml
@@ -7,6 +7,10 @@
"是的,取消"
"您确定要取消加入此房间的请求吗?"
"取消加入申请"
+ "是的,拒绝并屏蔽"
+ "您确定要拒绝加入此房间的邀请吗?这也将阻止%1$s 与您联系或邀请您加入房间。"
+ "拒绝邀请并屏蔽"
+ "拒绝并屏蔽"
"加入房间失败。"
"要么此房间仅限受邀者,要么可能在空间层级有加入限制。"
"忘记这个房间"
@@ -17,6 +21,8 @@
"消息(可选)"
"如果您的请求被接受,您将收到加入房间的邀请。"
"加入请求已发送"
+ "无法显示房间预览。这可能是由于网络或服务器问题造成的。"
+ "无法显示此房间预览"
"%1$s 尚不支持空间。您可以通过 Web 端访问空间"
"空间尚不支持"
"点击下面的按钮,系统将通知聊天室管理员。获得批准后将能够加入对话。"
diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt
index 8a475668ae..cabab56802 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
@@ -58,6 +58,7 @@ import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
+import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
@@ -913,7 +914,7 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.success(
- aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.KnockRestricted(emptyList())))
+ aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.KnockRestricted(persistentListOf())))
)
}
)
@@ -933,7 +934,7 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.success(
- aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Restricted(emptyList())))
+ aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Restricted(persistentListOf())))
)
}
)
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt
index 334bb531ae..2ee070d41c 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt
@@ -38,6 +38,7 @@ import io.element.android.libraries.designsystem.components.async.AsyncIndicator
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
import io.element.android.libraries.designsystem.components.avatar.Avatar
+import io.element.android.libraries.designsystem.components.avatar.AvatarRow
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.preview.ElementPreview
diff --git a/features/leaveroom/api/src/main/res/values-eu/translations.xml b/features/leaveroom/api/src/main/res/values-eu/translations.xml
index df735861b8..16fee54c82 100644
--- a/features/leaveroom/api/src/main/res/values-eu/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-eu/translations.xml
@@ -3,5 +3,7 @@
"Ziur elkarrizketa utzi nahi duzula? Ez da publikoa eta ezingo zara berriro batu gonbidapenik gabe."
"Ziur gelatik irten nahi duzula? Dagoen pertsona bakarra zara. Ateratzen bazara ezingo da inor batu etorkizunean, ezta zeu ere."
"Ziur gelatik irten nahi duzula? Gela hau ez da publikoa eta ezingo zara berriro batu gonbidapenik gabe."
+ "Aukeratu jabeak"
+ "Eskualdatu jabetza"
"Ziur gelatik atera nahi duzula?"
diff --git a/features/leaveroom/api/src/main/res/values-sv/translations.xml b/features/leaveroom/api/src/main/res/values-sv/translations.xml
index c80d716329..2d26a6ec32 100644
--- a/features/leaveroom/api/src/main/res/values-sv/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-sv/translations.xml
@@ -3,5 +3,8 @@
"Är du säker på att du vill lämna den här konversationen? Den här konversationen är inte offentlig och du kommer inte att kunna gå med igen utan en inbjudan."
"Är du säker på att du vill lämna det här rummet? Du är den enda personen här. Om du lämnar kommer ingen att kunna gå med i framtiden, inklusive du."
"Är du säker på att du vill lämna det här rummet? Detta rum är inte offentligt och du kommer inte att kunna gå med igen utan en inbjudan."
+ "Välj ägare"
+ "Du är den enda ägaren av det här rummet. Du måste överföra ägarskapet till någon annan innan du lämnar rummet."
+ "Överför ägarskap"
"Är du säker på att du vill lämna rummet?"
diff --git a/features/leaveroom/api/src/main/res/values-zh-rTW/translations.xml b/features/leaveroom/api/src/main/res/values-zh-rTW/translations.xml
index f561b08adc..96e3b576c4 100644
--- a/features/leaveroom/api/src/main/res/values-zh-rTW/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-zh-rTW/translations.xml
@@ -3,5 +3,8 @@
"您確定要離開對話嗎?此對話不是公開的,如果沒有收到邀請,您無法重新加入。"
"您確定要離開聊天室嗎?這裡只有您一個人。如果您離開了,包含您在內的所有人都無法再進入此聊天室。"
"您確定要離開聊天室嗎?此聊天室不是公開的,如果沒有收到邀請,您無法重新加入。"
+ "選擇擁有者"
+ "您是此聊天室的唯一擁有者。離開聊天室前,您必須將所有權移轉給其他人。"
+ "轉移所有權"
"您確定要離開聊天室嗎?"
diff --git a/features/leaveroom/impl/build.gradle.kts b/features/leaveroom/impl/build.gradle.kts
index 1a31023f04..ff9eb5de95 100644
--- a/features/leaveroom/impl/build.gradle.kts
+++ b/features/leaveroom/impl/build.gradle.kts
@@ -24,6 +24,7 @@ dependencies {
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
+ implementation(projects.libraries.push.api)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
@@ -32,5 +33,6 @@ dependencies {
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.libraries.push.test)
testImplementation(projects.tests.testutils)
}
diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt
index 2f1bab248f..d1575aac91 100644
--- a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt
+++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt
@@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.usersWithRole
+import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -33,6 +34,7 @@ import javax.inject.Inject
class LeaveRoomPresenter @Inject constructor(
private val client: MatrixClient,
private val dispatchers: CoroutineDispatchers,
+ private val notificationConversationService: NotificationConversationService,
) : Presenter {
@Composable
override fun present(): LeaveRoomState {
@@ -78,6 +80,7 @@ class LeaveRoomPresenter @Inject constructor(
client.getRoom(roomId)!!.use { room ->
room
.leave()
+ .onSuccess { notificationConversationService.onLeftRoom(client.sessionId, roomId) }
.onFailure { Timber.e(it, "Error while leaving room ${room.roomId}") }
.getOrThrow()
}
diff --git a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt
index 498edc801a..9e8c4113ae 100644
--- a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt
+++ b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt
@@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
+import io.element.android.libraries.push.test.notifications.conversations.FakeNotificationConversationService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
@@ -209,4 +210,5 @@ private fun TestScope.createLeaveRoomPresenter(
): LeaveRoomPresenter = LeaveRoomPresenter(
client = client,
dispatchers = testCoroutineDispatchers(false),
+ notificationConversationService = FakeNotificationConversationService(),
)
diff --git a/features/location/api/build.gradle.kts b/features/location/api/build.gradle.kts
index d299887165..4ce33a748a 100644
--- a/features/location/api/build.gradle.kts
+++ b/features/location/api/build.gradle.kts
@@ -65,6 +65,7 @@ dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.core)
+ implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiStrings)
implementation(libs.coil.compose)
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/SendLocationEntryPoint.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/SendLocationEntryPoint.kt
index c611340aea..1528e6cb14 100644
--- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/SendLocationEntryPoint.kt
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/SendLocationEntryPoint.kt
@@ -7,11 +7,19 @@
package io.element.android.features.location.api
-import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import io.element.android.libraries.architecture.FeatureEntryPoint
+import io.element.android.libraries.matrix.api.timeline.Timeline
/**
* The "Send location" screen.
*
* Allows a user to share a location message within a room.
*/
-interface SendLocationEntryPoint : SimpleFeatureEntryPoint
+interface SendLocationEntryPoint : FeatureEntryPoint {
+ fun builder(timelineMode: Timeline.Mode): Builder
+ interface Builder {
+ fun build(parentNode: Node, buildContext: BuildContext): Node
+ }
+}
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPoint.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPoint.kt
index e4c7b14768..cf601a412e 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPoint.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPoint.kt
@@ -13,12 +13,21 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.location.api.SendLocationEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.matrix.api.timeline.Timeline
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultSendLocationEntryPoint @Inject constructor() : SendLocationEntryPoint {
- override fun createNode(
- parentNode: Node,
- buildContext: BuildContext
- ): SendLocationNode = parentNode.createNode(buildContext)
+ override fun builder(timelineMode: Timeline.Mode): SendLocationEntryPoint.Builder {
+ return Builder(timelineMode)
+ }
+
+ class Builder(private val timelineMode: Timeline.Mode) : SendLocationEntryPoint.Builder {
+ override fun build(parentNode: Node, buildContext: BuildContext): Node {
+ return parentNode.createNode(
+ buildContext = buildContext,
+ plugins = listOf(SendLocationNode.Inputs(timelineMode))
+ )
+ }
+ }
}
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationNode.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationNode.kt
index 4fd438c96e..97e78fcb07 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
@@ -17,16 +17,25 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.anvilannotations.ContributesNode
+import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class)
class SendLocationNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- private val presenter: SendLocationPresenter,
+ presenterFactory: SendLocationPresenter.Factory,
analyticsService: AnalyticsService,
) : Node(buildContext, plugins = plugins) {
+ data class Inputs(
+ val timelineMode: Timeline.Mode,
+ ) : NodeInputs
+
+ private val presenter = presenterFactory.create(inputs().timelineMode)
+
init {
lifecycle.subscribe(
onResume = {
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 e4859da08d..2619352af1 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt
@@ -15,6 +15,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import 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
@@ -23,22 +26,30 @@ import io.element.android.features.location.impl.common.permissions.PermissionsP
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.meta.BuildMeta
+import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.location.AssetType
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.launch
-import javax.inject.Inject
-class SendLocationPresenter @Inject constructor(
+class SendLocationPresenter @AssistedInject constructor(
permissionsPresenterFactory: PermissionsPresenter.Factory,
private val room: JoinedRoom,
+ @Assisted private val timelineMode: Timeline.Mode,
private val analyticsService: AnalyticsService,
private val messageComposerContext: MessageComposerContext,
private val locationActions: LocationActions,
private val buildMeta: BuildMeta,
) : Presenter {
+ @AssistedFactory
+ interface Factory {
+ fun create(timelineMode: Timeline.Mode): SendLocationPresenter
+ }
+
private val permissionsPresenter = permissionsPresenterFactory.create(MapDefaults.permissions)
@Composable
@@ -104,14 +115,16 @@ class SendLocationPresenter @Inject constructor(
when (mode) {
SendLocationState.Mode.PinLocation -> {
val geoUri = event.cameraPosition.toGeoUri()
- room.liveTimeline.sendLocation(
- body = generateBody(geoUri),
- geoUri = geoUri,
- description = null,
- zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
- assetType = AssetType.PIN,
- inReplyToEventId = inReplyToEventId,
- )
+ getTimeline().flatMap {
+ it.sendLocation(
+ body = generateBody(geoUri),
+ geoUri = geoUri,
+ description = null,
+ zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
+ assetType = AssetType.PIN,
+ inReplyToEventId = inReplyToEventId,
+ )
+ }
analyticsService.capture(
Composer(
inThread = messageComposerContext.composerMode.inThread,
@@ -123,14 +136,16 @@ class SendLocationPresenter @Inject constructor(
}
SendLocationState.Mode.SenderLocation -> {
val geoUri = event.toGeoUri()
- room.liveTimeline.sendLocation(
- body = generateBody(geoUri),
- geoUri = geoUri,
- description = null,
- zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
- assetType = AssetType.SENDER,
- inReplyToEventId = inReplyToEventId,
- )
+ getTimeline().flatMap {
+ it.sendLocation(
+ body = generateBody(geoUri),
+ geoUri = geoUri,
+ description = null,
+ zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
+ assetType = AssetType.SENDER,
+ inReplyToEventId = inReplyToEventId,
+ )
+ }
analyticsService.capture(
Composer(
inThread = messageComposerContext.composerMode.inThread,
@@ -142,6 +157,13 @@ class SendLocationPresenter @Inject constructor(
}
}
}
+
+ private suspend fun getTimeline(): Result {
+ return when (timelineMode) {
+ is Timeline.Mode.Thread -> room.createTimeline(CreateTimelineParams.Threaded(timelineMode.threadRootId))
+ else -> Result.success(room.liveTimeline)
+ }
+ }
}
private fun SendLocationEvents.SendLocation.toGeoUri(): String = location?.toGeoUri() ?: cameraPosition.toGeoUri()
diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt
index 543d71eae1..4847942d67 100644
--- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt
+++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt
@@ -23,6 +23,7 @@ import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.location.AssetType
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.core.aBuildMeta
@@ -55,6 +56,7 @@ class SendLocationPresenterTest {
override fun create(permissions: List): PermissionsPresenter = fakePermissionsPresenter
},
room = joinedRoom,
+ timelineMode = Timeline.Mode.Live,
analyticsService = fakeAnalyticsService,
messageComposerContext = fakeMessageComposerContext,
locationActions = fakeLocationActions,
diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts
index ad77b20e60..4260e9e792 100644
--- a/features/lockscreen/impl/build.gradle.kts
+++ b/features/lockscreen/impl/build.gradle.kts
@@ -27,6 +27,7 @@ dependencies {
implementation(projects.appconfig)
implementation(projects.features.enterprise.api)
implementation(projects.libraries.core)
+ implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt
index b3086ddb9e..a2ccccf984 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt
@@ -7,44 +7,39 @@
package io.element.android.features.lockscreen.impl.storage
-import android.content.Context
-import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
-import androidx.datastore.preferences.preferencesDataStore
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.lockscreen.impl.LockScreenConfig
import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
-import io.element.android.libraries.di.SingleIn
+import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import javax.inject.Inject
-private val Context.dataStore: DataStore by preferencesDataStore(name = "pin_code_store")
-
-@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class PreferencesLockScreenStore @Inject constructor(
- @ApplicationContext private val context: Context,
+ preferenceDataStoreFactory: PreferenceDataStoreFactory,
private val lockScreenConfig: LockScreenConfig,
) : LockScreenStore {
+ private val dataStore = preferenceDataStoreFactory.create("pin_code_store")
+
private val pinCodeKey = stringPreferencesKey("encoded_pin_code")
private val remainingAttemptsKey = intPreferencesKey("remaining_pin_code_attempts")
private val biometricUnlockKey = booleanPreferencesKey("biometric_unlock_enabled")
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
- return context.dataStore.data.map { preferences ->
+ return dataStore.data.map { preferences ->
preferences.getRemainingPinCodeAttemptsNumber()
}.first()
}
override suspend fun onWrongPin() {
- context.dataStore.edit { preferences ->
+ dataStore.edit { preferences ->
val current = preferences.getRemainingPinCodeAttemptsNumber()
val remaining = (current - 1).coerceAtLeast(0)
preferences[remainingAttemptsKey] = remaining
@@ -52,43 +47,43 @@ class PreferencesLockScreenStore @Inject constructor(
}
override suspend fun resetCounter() {
- context.dataStore.edit { preferences ->
+ dataStore.edit { preferences ->
preferences[remainingAttemptsKey] = lockScreenConfig.maxPinCodeAttemptsBeforeLogout
}
}
override suspend fun getEncryptedCode(): String? {
- return context.dataStore.data.map { preferences ->
+ return dataStore.data.map { preferences ->
preferences[pinCodeKey]
}.first()
}
override suspend fun saveEncryptedPinCode(pinCode: String) {
- context.dataStore.edit { preferences ->
+ dataStore.edit { preferences ->
preferences[pinCodeKey] = pinCode
}
}
override suspend fun deleteEncryptedPinCode() {
- context.dataStore.edit { preferences ->
+ dataStore.edit { preferences ->
preferences.remove(pinCodeKey)
}
}
override fun hasPinCode(): Flow {
- return context.dataStore.data.map { preferences ->
+ return dataStore.data.map { preferences ->
preferences[pinCodeKey] != null
}
}
override fun isBiometricUnlockAllowed(): Flow {
- return context.dataStore.data.map { preferences ->
+ return dataStore.data.map { preferences ->
preferences[biometricUnlockKey] ?: false
}
}
override suspend fun setIsBiometricUnlockAllowed(isAllowed: Boolean) {
- context.dataStore.edit { preferences ->
+ dataStore.edit { preferences ->
preferences[biometricUnlockKey] = isAllowed
}
}
diff --git a/features/lockscreen/impl/src/main/res/values-fa/translations.xml b/features/lockscreen/impl/src/main/res/values-fa/translations.xml
index 8e57ec8d68..56dc91e835 100644
--- a/features/lockscreen/impl/src/main/res/values-fa/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-fa/translations.xml
@@ -24,6 +24,14 @@
"پینها مطابق نیستند"
"برای ادامه باید دوباره وارد شده و پینی جدید ایجاد کنید"
"دارید خارج میشوید"
+
+ - "شما %1$d تلاش برای باز کردن قفل دارید"
+ - "شما %1$d تلاش برای باز کردن قفل دارید"
+
+
+ - "پین اشتباه است. شما %1$d شانس دیگر دارید"
+ - "پین اشتباه است. شما %1$d شانس دیگر دارید"
+
"استفاده از زیستسنجی"
"استفاده از پین"
"خارج شدن…"
diff --git a/features/lockscreen/impl/src/main/res/values-uz/translations.xml b/features/lockscreen/impl/src/main/res/values-uz/translations.xml
index b9f1c4f5cb..e15d51c8bc 100644
--- a/features/lockscreen/impl/src/main/res/values-uz/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-uz/translations.xml
@@ -1,4 +1,33 @@
+ "biometrik autentifikatsiya"
+ "biometrik qulf ochish"
+ "PIN kodni o\'zgartirish"
+ "Biometrik qulfni ochishga ruxsat bering"
+ "PIN-kodni olib tashlang"
+ "Haqiqatan ham PIN kodni olib tashlamoqchimisiz?"
+ "PIN kod olib tashlansinmi?"
+ "Ruxsat berish %1$s"
+ "Men PIN kod ishlatishni maʼqul koʻraman"
+ "Oʻzingizga vaqt tejang va har safar ilovani ochish uchun %1$s dan foydalaning"
+ "PIN kodni tanlang"
+ "PIN kodni tasdiqlang"
+ "Qulflash %1$s suhbatlaringizga qoʻshimcha xavfsizlik qoʻshish uchun.
+
+Esda qoladigan biror narsani tanlang. Agar ushbu PIN kodni unutib qolsangiz, dasturdan chiqib ketasiz."
+ "Xavfsizlik sabablari bilan buni PIN kodingiz sifatida tanlay olmaysiz"
+ "Boshqa PIN kod tanlang"
+ "Iltimos, bir xil PIN kodni ikkita marta kiriting"
+ "PIN kodlar bir-biriga mos kelmadi"
+ "Davom etish uchun qayta kirishingiz va yangi PIN yaratishingiz kerak boʻladi."
+ "Siz tizimdan chiqmoqdasiz"
+
+ - "Sizda %1$d ta ochishga urinish mavjud"
+ - "Sizda %1$d ta ochishga urinish mavjud"
+
+
+ - "Notoʻgʻri PIN. Sizda yana %1$d ta imkoniyat bor"
+ - "Notoʻgʻri PIN. Sizda yana %1$d ta imkoniyat bor"
+
"Chiqish…"
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingLogoResIdProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingLogoResIdProvider.kt
new file mode 100644
index 0000000000..03cebff188
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingLogoResIdProvider.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.login.impl.screens.onboarding
+
+import android.annotation.SuppressLint
+import android.content.Context
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.ApplicationContext
+import javax.inject.Inject
+
+fun interface OnBoardingLogoResIdProvider {
+ fun get(): Int?
+}
+
+@ContributesBinding(AppScope::class)
+class DefaultOnBoardingLogoResIdProvider @Inject constructor(
+ @ApplicationContext private val context: Context,
+) : OnBoardingLogoResIdProvider {
+ @SuppressLint("DiscouragedApi")
+ override fun get(): Int? {
+ val resId = context.resources
+ .getIdentifier("onboarding_logo", "drawable", context.packageName)
+ .takeIf { it != 0 }
+ return resId
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt
index 0e545f44e6..ba46e79b36 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
@@ -36,6 +36,7 @@ class OnBoardingPresenter @AssistedInject constructor(
private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val loginHelper: LoginHelper,
+ private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider,
) : Presenter {
@AssistedFactory
interface Factory {
@@ -81,6 +82,9 @@ class OnBoardingPresenter @AssistedInject constructor(
}
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
var showReportBug by rememberSaveable { mutableStateOf(false) }
+ val onBoardingLogoResId = remember {
+ onBoardingLogoResIdProvider.get()
+ }
val loginMode by loginHelper.collectLoginMode()
@@ -112,6 +116,7 @@ class OnBoardingPresenter @AssistedInject constructor(
canReportBug = canReportBug && showReportBug,
loginMode = loginMode,
version = buildMeta.versionName,
+ onBoardingLogoResId = onBoardingLogoResId,
eventSink = ::handleEvent,
)
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt
index 1e55c3af2d..c2896d4ea7 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt
@@ -7,6 +7,7 @@
package io.element.android.features.login.impl.screens.onboarding
+import androidx.annotation.DrawableRes
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData
@@ -18,6 +19,8 @@ data class OnBoardingState(
val canCreateAccount: Boolean,
val canReportBug: Boolean,
val version: String,
+ @DrawableRes
+ val onBoardingLogoResId: Int?,
val loginMode: AsyncData,
val eventSink: (OnBoardingEvents) -> Unit,
) {
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt
index cdf77da523..cc41e64480 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt
@@ -7,9 +7,11 @@
package io.element.android.features.login.impl.screens.onboarding
+import androidx.annotation.DrawableRes
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.designsystem.R
open class OnBoardingStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -20,6 +22,7 @@ open class OnBoardingStateProvider : PreviewParameterProvider {
anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true),
anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true, canReportBug = true),
anOnBoardingState(defaultAccountProvider = "element.io", canCreateAccount = false, canReportBug = true),
+ anOnBoardingState(customLogoResId = R.drawable.sample_background),
)
}
@@ -31,6 +34,8 @@ fun anOnBoardingState(
canCreateAccount: Boolean = false,
canReportBug: Boolean = false,
version: String = "1.0.0",
+ @DrawableRes
+ customLogoResId: Int? = null,
loginMode: AsyncData = AsyncData.Uninitialized,
eventSink: (OnBoardingEvents) -> Unit = {},
) = OnBoardingState(
@@ -42,5 +47,6 @@ fun anOnBoardingState(
canReportBug = canReportBug,
version = version,
loginMode = loginMode,
+ onBoardingLogoResId = customLogoResId,
eventSink = eventSink,
)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt
index 6c67e75cd1..4c44ee132a 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt
@@ -7,6 +7,7 @@
package io.element.android.features.login.impl.screens.onboarding
+import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -19,9 +20,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.BiasAlignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -67,8 +70,15 @@ fun OnBoardingView(
) {
OnBoardingPage(
modifier = modifier,
+ renderBackground = state.onBoardingLogoResId == null,
content = {
- OnBoardingContent(state = state)
+ if (state.onBoardingLogoResId != null) {
+ OnBoardingLogo(
+ onBoardingLogoResId = state.onBoardingLogoResId,
+ )
+ } else {
+ OnBoardingContent(state = state)
+ }
LoginModeView(
loginMode = state.loginMode,
onClearError = {
@@ -139,6 +149,24 @@ private fun OnBoardingContent(state: OnBoardingState) {
}
}
+@Composable
+private fun OnBoardingLogo(
+ onBoardingLogoResId: Int,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ Image(
+ painter = painterResource(id = onBoardingLogoResId),
+ contentDescription = null
+ )
+ }
+}
+
@Composable
private fun OnBoardingButtons(
state: OnBoardingState,
diff --git a/features/login/impl/src/main/res/raw/keep.xml b/features/login/impl/src/main/res/raw/keep.xml
new file mode 100644
index 0000000000..478b6d4016
--- /dev/null
+++ b/features/login/impl/src/main/res/raw/keep.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/features/login/impl/src/main/res/values-da/translations.xml b/features/login/impl/src/main/res/values-da/translations.xml
index af64068d32..cffa9c7e2e 100644
--- a/features/login/impl/src/main/res/values-da/translations.xml
+++ b/features/login/impl/src/main/res/values-da/translations.xml
@@ -13,6 +13,7 @@
"Anden"
"Brug en anden kontoudbyder, f.eks. din egen private server eller en arbejdskonto."
"Skift kontoudbyder"
+ "Google Play"
"Element Pro-appen er påkrævet på %1$s Download den venligst fra din app store."
"Element Pro kræves"
"Vi kunne ikke nå denne hjemmeserver. Kontroller, at du har indtastet hjemmeserverens URL korrekt. Hvis URL-adressen er korrekt, skal du kontakte administratoren på din hjemmeserver for at få yderligere hjælp."
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 ecf908f21f..6ab86a1687 100644
--- a/features/login/impl/src/main/res/values-eu/translations.xml
+++ b/features/login/impl/src/main/res/values-eu/translations.xml
@@ -10,6 +10,10 @@
"Beste bat"
"Erabili beste kontu-hornitzaile bat, hala nola zure zerbitzari pribatua edo laneko kontu bat."
"Aldatu kontu-hornitzailea"
+ "Google Play"
+ "Element Pro behar da"
+ "Hautatutako kontu-hornitzailea ez da bateragarria Sliding Sync-ekin. Beharrezkoa da zerbitzaria bertsio-berritzea %1$s erabiltzeko."
+ "Ez da %1$s kontu-hornitzailea onartzen."
"Zerbitzariaren URLa"
"Sartu domeinu-helbide bat."
"Zein da zure zerbitzariaren helbidea?"
@@ -17,10 +21,12 @@
"Sortu kontua"
"Kontu hau desaktibatuta dago."
"Erabiltzaile-izena edo/eta pasahitza okerrak"
+ "Hautatutako zerbitzaria ez da bateragarria pasahitz edo OIDC saio-hasierarekin. Jarri harremanetan administratzailearekin edo aukeratu beste zerbitzari bat."
"Sartu zure datuak"
"Matrix komunikazio seguru eta deszentralizaturako sare irekia da."
"Ongi etorri!"
"Hasi saioa %1$s(e)n"
+ "%1$s bertsioa"
"Hasi saioa eskuz"
"Hasi saioa QR kodearekin"
"Sortu kontua"
@@ -67,5 +73,6 @@ Saiatu saioa eskuz hasten, edo eskaneatu QR kodea beste gailu batean."
"Elementeko langileentzako zerbitzari pribatua."
"Matrix komunikazio seguru eta deszentralizaturako sare irekia da."
"%1$s(e)n saioa hastear zaude"
+ "Aukeratu kontu-hornitzailea"
"%1$s(e)n kontua sortzear zaude"
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 122d8bbca1..26936afbd7 100644
--- a/features/login/impl/src/main/res/values-fr/translations.xml
+++ b/features/login/impl/src/main/res/values-fr/translations.xml
@@ -13,6 +13,7 @@
"Autres"
"Utilisez un autre fournisseur de compte, tel que votre propre serveur privé ou un serveur professionnel."
"Changer de fournisseur de compte"
+ "Google Play"
"L’application Element Pro est requise sur %1$s. Veuillez la télécharger depuis le store."
"Element Pro est requis"
"Nous n’avons pas pu atteindre ce serveur d’accueil. Vérifiez que vous avez correctement saisi l’URL du serveur d’accueil. Si l’URL est correcte, contactez l’administrateur de votre serveur d’accueil pour obtenir de l’aide."
diff --git a/features/login/impl/src/main/res/values-sv/translations.xml b/features/login/impl/src/main/res/values-sv/translations.xml
index 7d59180093..33fb76b5bd 100644
--- a/features/login/impl/src/main/res/values-sv/translations.xml
+++ b/features/login/impl/src/main/res/values-sv/translations.xml
@@ -13,6 +13,9 @@
"Annan"
"Använd en annan kontoleverantör, till exempel din egen privata server eller ett jobbkonto."
"Byt kontoleverantör"
+ "Google Play"
+ "Element Pro-appen krävs på %1$s. Ladda ner den från butiken."
+ "Element Pro krävs"
"Vi kunde inte nå den här hemservern. Kontrollera att du har angett hemserverns URL korrekt. Om URL:en är korrekt kontaktar du administratören för hemservern för ytterligare hjälp."
"Sliding Sync är inte tillgängligt på grund av ett problem i .well-known-filen:
%1$s"
diff --git a/features/login/impl/src/main/res/values-uk/translations.xml b/features/login/impl/src/main/res/values-uk/translations.xml
index c81a3e34f2..697c21ab8e 100644
--- a/features/login/impl/src/main/res/values-uk/translations.xml
+++ b/features/login/impl/src/main/res/values-uk/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-uz/translations.xml b/features/login/impl/src/main/res/values-uz/translations.xml
index 25fc695452..dabdf1d3ed 100644
--- a/features/login/impl/src/main/res/values-uz/translations.xml
+++ b/features/login/impl/src/main/res/values-uz/translations.xml
@@ -32,6 +32,7 @@
"Eng tezkor %1$sga xush kelibsiz. Tezlik va oddylik uchun super zaryadlangan."
"%1$sga Xush kelibsiz. Tezlik va oddylik uchun o\'ta zaryadlangan."
"Elementingizda bo\'ling"
+ "Qayta urinib ko\'ring"
"Hisob provayderini o\'zgartiring"
"Element xodimlari uchun shaxsiy server."
"Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir."
diff --git a/features/login/impl/src/main/res/values-zh-rTW/translations.xml b/features/login/impl/src/main/res/values-zh-rTW/translations.xml
index f23089779e..23e290d69c 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
@@ -13,10 +13,16 @@
"其他"
"使用不同的帳戶提供者,例如您自己的伺服器或工作帳號。"
"更改帳號提供者"
+ "Google Play"
+ "%1$s 需要 Element Pro 應用程式。請從商店下載。"
+ "需要 Element Pro"
"我們無法連線至此家伺服器。請檢查您是否已正確輸入家伺服器 URL。若 URL 正確,請聯絡您家伺服器的管理員以取得進一步協助。"
"因為 well-known 檔案的問題,伺服器不可用:
%1$s"
"選定的帳號提供者不支援 sliding sync。必須升級伺服器才能使用 %1$s。"
+ "不允許 %1$s 連線至 %2$s。"
+ "應用程式已設定為允許:%1$s。"
+ "不允許帳號提供者 %1$s。"
"家伺服器 URL"
"輸入網域位址。"
"您的伺服器地址?"
@@ -31,7 +37,9 @@
"Matrix 是一個開放網路,為了安全且去中心化的通訊而生。"
"歡迎回來!"
"登入 %1$s"
+ "版本 %1$s"
"手動登入"
+ "登入至 %1$s"
"使用 QR code 登入"
"建立帳號"
"歡迎使用有史以來最快的 %1$s。速度超快,操作簡便。"
@@ -85,5 +93,6 @@
"Matrix 是一個開放網路,為了安全且去中心化的通訊而生。"
"您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。"
"您即將登入 %1$s"
+ "選擇帳號提供者"
"您即將在 %1$s 建立帳號"
diff --git a/features/login/impl/src/main/res/values-zh/translations.xml b/features/login/impl/src/main/res/values-zh/translations.xml
index 44d5d48574..ec6bc91ff7 100644
--- a/features/login/impl/src/main/res/values-zh/translations.xml
+++ b/features/login/impl/src/main/res/values-zh/translations.xml
@@ -13,10 +13,15 @@
"其他"
"使用其他账户提供商,例如您自己的私人服务器或工作账户。"
"更改账户提供方"
+ "需要 Element Pro 版"
"我们无法访问此服务器。请检查您输入的服务器网址是否正确。如果 URL 正确,请联系您的服务器管理员寻求进一步帮助。"
- "由于 Well Known 文件中的问题,Sliding Sync 不可用:
+ "由于 .well-known 文件中存在问题,服务器不可用:
%1$s"
+ "所选账户提供商不支持跨屏同步。需要升级服务器才能使用%1$s。"
+ "%1$s不允许连接到%2$s。"
+ "账户提供商%1$s 不被允许。"
"服务器网址"
+ "输入域名地址。"
"您的服务器地址是什么?"
"选择服务器"
"创建账户"
@@ -29,7 +34,9 @@
"Matrix 是一个用于安全、去中心化通信的开放网络。"
"欢迎回来!"
"登录到 %1$s"
+ "版本%1$s"
"手动登录"
+ "登录到 %1$s"
"使用二维码登录"
"创建账户"
"欢迎使用 %1$s,快而简约的消息应用。"
@@ -83,5 +90,6 @@
"Matrix 是一个用于安全、去中心化通信的开放网络。"
"这是您的对话将存在的地方,就像您使用电子邮件提供方来保存电子邮件一样。"
"即将登录 %1$s"
+ "选择账户提供商"
"即将在 %1$s 上创建一个账户"
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/DefaultOnBoardingLogoResIdProviderTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/DefaultOnBoardingLogoResIdProviderTest.kt
new file mode 100644
index 0000000000..7727583468
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/DefaultOnBoardingLogoResIdProviderTest.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.login.impl.screens.onboarding
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DefaultOnBoardingLogoResIdProviderTest {
+ @Test
+ fun `when onboarding_logo resource does not exist, get() returns null`() {
+ val context = InstrumentationRegistry.getInstrumentation().context
+ val sut = DefaultOnBoardingLogoResIdProvider(context)
+ val result = sut.get()
+ assertThat(result).isNull()
+ }
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt
index b47adf4cd8..21b1e9316d 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
@@ -84,6 +84,18 @@ class OnBoardingPresenterTest {
}
}
+ @Test
+ fun `present - on boarding logo`() = runTest {
+ val presenter = createPresenter(
+ onBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { 42 },
+ )
+ presenter.test {
+ skipItems(1)
+ val initialState = awaitItem()
+ assertThat(initialState.onBoardingLogoResId).isEqualTo(42)
+ }
+ }
+
@Test
fun `present - clicking on version 7 times has no effect if rageshake not available`() = runTest {
val presenter = createPresenter(
@@ -224,6 +236,7 @@ private fun createPresenter(
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
rageshakeFeatureAvailability: () -> Flow = { flowOf(true) },
loginHelper: LoginHelper = createLoginHelper(),
+ onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null },
) = OnBoardingPresenter(
params = params,
buildMeta = buildMeta,
@@ -234,6 +247,7 @@ private fun createPresenter(
),
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
loginHelper = loginHelper,
+ onBoardingLogoResIdProvider = onBoardingLogoResIdProvider,
)
fun createLoginHelper(
diff --git a/features/logout/impl/src/main/res/values-uz/translations.xml b/features/logout/impl/src/main/res/values-uz/translations.xml
index 4862d47399..f53c9c215b 100644
--- a/features/logout/impl/src/main/res/values-uz/translations.xml
+++ b/features/logout/impl/src/main/res/values-uz/translations.xml
@@ -4,6 +4,14 @@
"Tizimdan chiqish"
"Tizimdan chiqish"
"Chiqish…"
+ "Siz oxirgi sessiyangizdan chiqmoqdasiz. Agar hozir chiqib ketsangiz, shifrlangan xabarlaringizga kira olmaysiz."
+ "Siz zaxira nusxasini oʻchirdingiz"
+ "Kalitlaringiz hamon zaxiralanmoqda"
+ "Tizimdan chiqishdan oldin bu jarayon tugashini kuting."
+ "Kalitlaringiz hamon zaxiralanmoqda"
"Tizimdan chiqish"
+ "Siz oxirgi sessiyangizdan chiqmoqdasiz. Agar hozir chiqib ketsangiz, shifrlangan xabarlaringizga kira olmaysiz."
+ "Qayta tiklash sozlanmagan"
+ "Siz oxirgi sessiyangizdan chiqmoqdasiz. Agar hozir chiqib ketsangiz, shifrlangan xabarlaringizga kira olmay qolishingiz mumkin."
"Zaxira kalitingizni saqladingizmi?"
diff --git a/features/messages/api/build.gradle.kts b/features/messages/api/build.gradle.kts
index 7eefee11cc..a6a2edcf6d 100644
--- a/features/messages/api/build.gradle.kts
+++ b/features/messages/api/build.gradle.kts
@@ -16,6 +16,7 @@ android {
dependencies {
implementation(projects.libraries.architecture)
+ implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.mediaviewer.api)
implementation(projects.libraries.preferences.api)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerEvents.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerEvents.kt
similarity index 93%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerEvents.kt
rename to features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerEvents.kt
index 30053ba7b9..9586fd0bf7 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerEvents.kt
+++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerEvents.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.messages.impl.voicemessages.composer
+package io.element.android.features.messages.api.timeline.voicemessages.composer
import androidx.lifecycle.Lifecycle
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerPresenter.kt
new file mode 100644
index 0000000000..0e464e93b8
--- /dev/null
+++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerPresenter.kt
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.messages.api.timeline.voicemessages.composer
+
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.matrix.api.timeline.Timeline
+
+fun interface VoiceMessageComposerPresenter : Presenter {
+ interface Factory {
+ fun create(timelineMode: Timeline.Mode): VoiceMessageComposerPresenter
+ }
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerState.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerState.kt
similarity index 87%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerState.kt
rename to features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerState.kt
index 2e8c4caa87..e78a2b5611 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerState.kt
+++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerState.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.messages.impl.voicemessages.composer
+package io.element.android.features.messages.api.timeline.voicemessages.composer
import androidx.compose.runtime.Stable
import io.element.android.libraries.textcomposer.model.VoiceMessageState
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerStateProvider.kt
similarity index 80%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt
rename to features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerStateProvider.kt
index 534c45dade..e1bfee9a7f 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt
+++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerStateProvider.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.messages.impl.voicemessages.composer
+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
@@ -13,14 +13,14 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState
import kotlinx.collections.immutable.toPersistentList
import kotlin.time.Duration.Companion.seconds
-internal open class VoiceMessageComposerStateProvider : PreviewParameterProvider {
+open class VoiceMessageComposerStateProvider : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(duration = 61.seconds, levels = aWaveformLevels)),
)
}
-internal fun aVoiceMessageComposerState(
+fun aVoiceMessageComposerState(
voiceMessageState: VoiceMessageState = VoiceMessageState.Idle,
keepScreenOn: Boolean = false,
showPermissionRationaleDialog: Boolean = false,
@@ -33,7 +33,7 @@ internal fun aVoiceMessageComposerState(
eventSink = {},
)
-internal fun aVoiceMessagePreviewState() = VoiceMessageState.Preview(
+fun aVoiceMessagePreviewState() = VoiceMessageState.Preview(
isSending = false,
isPlaying = false,
showCursor = false,
diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts
index 00d8e3f765..1519f204f6 100644
--- a/features/messages/impl/build.gradle.kts
+++ b/features/messages/impl/build.gradle.kts
@@ -50,6 +50,7 @@ dependencies {
implementation(projects.libraries.voiceplayer.api)
implementation(projects.libraries.voicerecorder.api)
implementation(projects.libraries.mediaplayer.api)
+ implementation(projects.libraries.push.api)
implementation(projects.libraries.uiUtils)
implementation(projects.libraries.testtags)
implementation(projects.features.networkmonitor.api)
@@ -76,6 +77,7 @@ dependencies {
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.dateformatter.test)
+ testImplementation(projects.libraries.push.test)
testImplementation(projects.features.location.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.messages.test)
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 a0df0615eb..266d2459f0 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
@@ -38,6 +38,7 @@ import io.element.android.features.messages.impl.forward.ForwardMessagesNode
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
import io.element.android.features.messages.impl.pinned.list.PinnedMessagesListNode
import io.element.android.features.messages.impl.report.ReportMessageNode
+import io.element.android.features.messages.impl.threads.ThreadedMessagesNode
import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode
import io.element.android.features.messages.impl.timeline.model.TimelineItem
@@ -65,6 +66,7 @@ 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.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.media.MediaSource
@@ -139,7 +141,7 @@ class MessagesFlowNode @AssistedInject constructor(
) : NavTarget
@Parcelize
- data class AttachmentPreview(val attachment: Attachment) : NavTarget
+ data class AttachmentPreview(val timelineMode: Timeline.Mode, val attachment: Attachment) : NavTarget
@Parcelize
data class LocationViewer(val location: Location, val description: String?) : NavTarget
@@ -154,19 +156,22 @@ class MessagesFlowNode @AssistedInject constructor(
data class ReportMessage(val eventId: EventId, val senderId: UserId) : NavTarget
@Parcelize
- data object SendLocation : NavTarget
+ data class SendLocation(val timelineMode: Timeline.Mode) : NavTarget
@Parcelize
- data object CreatePoll : NavTarget
+ data class CreatePoll(val timelineMode: Timeline.Mode) : NavTarget
@Parcelize
- data class EditPoll(val eventId: EventId) : NavTarget
+ data class EditPoll(val timelineMode: Timeline.Mode, val eventId: EventId) : NavTarget
@Parcelize
data object PinnedMessagesList : NavTarget
@Parcelize
data object KnockRequestsList : NavTarget
+
+ @Parcelize
+ data class OpenThread(val threadRootId: ThreadId, val focusedEventId: EventId?) : NavTarget
}
private val callbacks = plugins()
@@ -211,15 +216,18 @@ class MessagesFlowNode @AssistedInject constructor(
callbacks.forEach { it.onRoomDetailsClick() }
}
- override fun onEventClick(isLive: Boolean, event: TimelineItem.Event): Boolean {
+ override fun onEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean {
return processEventClick(
- timelineMode = if (isLive) Timeline.Mode.LIVE else Timeline.Mode.FOCUSED_ON_EVENT,
+ timelineMode = timelineMode,
event = event,
)
}
override fun onPreviewAttachments(attachments: ImmutableList) {
- backstack.push(NavTarget.AttachmentPreview(attachments.first()))
+ backstack.push(NavTarget.AttachmentPreview(
+ attachment = attachments.first(),
+ timelineMode = Timeline.Mode.Live,
+ ))
}
override fun onUserDataClick(userId: UserId) {
@@ -243,15 +251,15 @@ class MessagesFlowNode @AssistedInject constructor(
}
override fun onSendLocationClick() {
- backstack.push(NavTarget.SendLocation)
+ backstack.push(NavTarget.SendLocation(Timeline.Mode.Live))
}
override fun onCreatePollClick() {
- backstack.push(NavTarget.CreatePoll)
+ backstack.push(NavTarget.CreatePoll(Timeline.Mode.Live))
}
override fun onEditPollClick(eventId: EventId) {
- backstack.push(NavTarget.EditPoll(eventId))
+ backstack.push(NavTarget.EditPoll(Timeline.Mode.Live, eventId))
}
override fun onJoinCallClick(roomId: RoomId) {
@@ -270,6 +278,10 @@ class MessagesFlowNode @AssistedInject constructor(
override fun onViewKnockRequests() {
backstack.push(NavTarget.KnockRequestsList)
}
+
+ override fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) {
+ backstack.push(NavTarget.OpenThread(threadRootId, focusedEventId))
+ }
}
val inputs = MessagesNode.Inputs(focusedEventId = navTarget.focusedEventId)
createNode(buildContext, listOf(callback, inputs))
@@ -298,7 +310,10 @@ class MessagesFlowNode @AssistedInject constructor(
.build()
}
is NavTarget.AttachmentPreview -> {
- val inputs = AttachmentsPreviewNode.Inputs(navTarget.attachment)
+ val inputs = AttachmentsPreviewNode.Inputs(
+ attachment = navTarget.attachment,
+ timelineMode = navTarget.timelineMode,
+ )
createNode(buildContext, listOf(inputs))
}
is NavTarget.LocationViewer -> {
@@ -327,24 +342,34 @@ class MessagesFlowNode @AssistedInject constructor(
val inputs = ReportMessageNode.Inputs(navTarget.eventId, navTarget.senderId)
createNode(buildContext, listOf(inputs))
}
- NavTarget.SendLocation -> {
- sendLocationEntryPoint.createNode(this, buildContext)
+ is NavTarget.SendLocation -> {
+ sendLocationEntryPoint
+ .builder(navTarget.timelineMode)
+ .build(this, buildContext)
}
- NavTarget.CreatePoll -> {
+ is NavTarget.CreatePoll -> {
createPollEntryPoint.nodeBuilder(this, buildContext)
- .params(CreatePollEntryPoint.Params(mode = CreatePollMode.NewPoll))
+ .params(CreatePollEntryPoint.Params(
+ timelineMode = navTarget.timelineMode,
+ mode = CreatePollMode.NewPoll
+ ))
.build()
}
is NavTarget.EditPoll -> {
createPollEntryPoint.nodeBuilder(this, buildContext)
- .params(CreatePollEntryPoint.Params(mode = CreatePollMode.EditPoll(eventId = navTarget.eventId)))
+ .params(
+ CreatePollEntryPoint.Params(
+ timelineMode = navTarget.timelineMode,
+ mode = CreatePollMode.EditPoll(eventId = navTarget.eventId)
+ )
+ )
.build()
}
NavTarget.PinnedMessagesList -> {
val callback = object : PinnedMessagesListNode.Callback {
override fun onEventClick(event: TimelineItem.Event) {
processEventClick(
- timelineMode = Timeline.Mode.PINNED_EVENTS,
+ timelineMode = Timeline.Mode.PinnedEvents,
event = event,
)
}
@@ -377,6 +402,69 @@ class MessagesFlowNode @AssistedInject constructor(
NavTarget.KnockRequestsList -> {
knockRequestsListEntryPoint.createNode(this, buildContext)
}
+ is NavTarget.OpenThread -> {
+ val inputs = ThreadedMessagesNode.Inputs(
+ threadRootEventId = navTarget.threadRootId,
+ focusedEventId = navTarget.focusedEventId,
+ )
+ val callback = object : ThreadedMessagesNode.Callback {
+ override fun onEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean {
+ return processEventClick(
+ timelineMode = timelineMode,
+ event = event,
+ )
+ }
+
+ override fun onPreviewAttachments(attachments: ImmutableList) {
+ backstack.push(NavTarget.AttachmentPreview(
+ attachment = attachments.first(),
+ timelineMode = Timeline.Mode.Thread(navTarget.threadRootId)
+ ))
+ }
+
+ override fun onUserDataClick(userId: UserId) {
+ callbacks.forEach { it.onUserDataClick(userId) }
+ }
+
+ override fun onPermalinkClick(data: PermalinkData) {
+ callbacks.forEach { it.onPermalinkClick(data, pushToBackstack = true) }
+ }
+
+ override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
+ backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo))
+ }
+
+ override fun onForwardEventClick(eventId: EventId) {
+ backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents = false))
+ }
+
+ override fun onReportMessage(eventId: EventId, senderId: UserId) {
+ backstack.push(NavTarget.ReportMessage(eventId, senderId))
+ }
+
+ override fun onSendLocationClick() {
+ backstack.push(NavTarget.SendLocation(Timeline.Mode.Thread(navTarget.threadRootId)))
+ }
+
+ override fun onCreatePollClick() {
+ backstack.push(NavTarget.CreatePoll(Timeline.Mode.Thread(navTarget.threadRootId)))
+ }
+
+ override fun onEditPollClick(eventId: EventId) {
+ backstack.push(NavTarget.EditPoll(Timeline.Mode.Thread(navTarget.threadRootId), eventId))
+ }
+
+ override fun onJoinCallClick(roomId: RoomId) {
+ val callType = CallType.RoomCall(
+ sessionId = matrixClient.sessionId,
+ roomId = roomId,
+ )
+ analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
+ elementCallEntryPoint.startCall(callType)
+ }
+ }
+ 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 d3c50bc133..ad8e6c6081 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
@@ -10,6 +10,7 @@ package io.element.android.features.messages.impl
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import kotlinx.collections.immutable.ImmutableList
@@ -21,4 +22,5 @@ interface MessagesNavigator {
fun onEditPollClick(eventId: EventId)
fun onPreviewAttachment(attachments: ImmutableList)
fun onNavigateToRoom(roomId: RoomId, 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 41b88be821..d286f8a4c7 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
@@ -34,6 +34,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
+import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
@@ -55,12 +56,14 @@ 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.BaseRoom
+import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.alias.matches
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.mediaplayer.api.MediaPlayer
import io.element.android.libraries.ui.strings.CommonStrings
@@ -75,9 +78,8 @@ class MessagesNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
@ApplicationContext private val context: Context,
- @SessionCoroutineScope
- private val sessionCoroutineScope: CoroutineScope,
- private val room: BaseRoom,
+ @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
+ private val room: JoinedRoom,
private val analyticsService: AnalyticsService,
messageComposerPresenterFactory: MessageComposerPresenter.Factory,
timelinePresenterFactory: TimelinePresenter.Factory,
@@ -89,11 +91,16 @@ class MessagesNode @AssistedInject constructor(
private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer,
private val roomMemberModerationRenderer: RoomMemberModerationRenderer,
) : Node(buildContext, plugins = plugins), MessagesNavigator {
+ private val timelineController = TimelineController(room, room.liveTimeline)
private val presenter = presenterFactory.create(
navigator = this,
- composerPresenter = messageComposerPresenterFactory.create(this),
- timelinePresenter = timelinePresenterFactory.create(this),
- actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
+ composerPresenter = messageComposerPresenterFactory.create(timelineController, this),
+ timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this),
+ actionListPresenter = actionListPresenterFactory.create(
+ postProcessor = TimelineItemActionPostProcessor.Default,
+ timelineMode = timelineController.mainTimelineMode()
+ ),
+ timelineController = timelineController,
)
private val callbacks = plugins()
@@ -103,7 +110,7 @@ class MessagesNode @AssistedInject constructor(
interface Callback : Plugin {
fun onRoomDetailsClick()
- fun onEventClick(isLive: Boolean, event: TimelineItem.Event): Boolean
+ fun onEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean
fun onPreviewAttachments(attachments: ImmutableList)
fun onUserDataClick(userId: UserId)
fun onPermalinkClick(data: PermalinkData)
@@ -116,6 +123,7 @@ class MessagesNode @AssistedInject constructor(
fun onJoinCallClick(roomId: RoomId)
fun onViewAllPinnedEvents()
fun onViewKnockRequests()
+ fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?)
}
override fun onBuilt() {
@@ -134,12 +142,12 @@ class MessagesNode @AssistedInject constructor(
callbacks.forEach { it.onRoomDetailsClick() }
}
- private fun onEventClick(isLive: Boolean, event: TimelineItem.Event): Boolean {
+ 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.
// - if a callback returns false, the other callback will not be invoked.
return callbacks.takeIf { it.isNotEmpty() }
- ?.map { it.onEventClick(isLive, event) }
+ ?.map { it.onEventClick(timelineMode, event) }
?.all { it }
.orFalse()
}
@@ -223,6 +231,10 @@ class MessagesNode @AssistedInject constructor(
}
}
+ override fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) {
+ callbacks.forEach { it.onOpenThread(threadRootId, focusedEventId) }
+ }
+
private fun onViewAllPinnedMessagesClick() {
callbacks.forEach { it.onViewAllPinnedEvents() }
}
@@ -265,7 +277,18 @@ class MessagesNode @AssistedInject constructor(
state = state,
onBackClick = this::navigateUp,
onRoomDetailsClick = this::onRoomDetailsClick,
- onEventContentClick = this::onEventClick,
+ onEventContentClick = { isLive, event ->
+ if (isLive) {
+ onEventClick(timelineController.mainTimelineMode(), event)
+ } else {
+ val detachedTimelineMode = timelineController.detachedTimelineMode()
+ if (detachedTimelineMode != null) {
+ onEventClick(detachedTimelineMode, event)
+ } else {
+ false
+ }
+ }
+ },
onUserDataClick = this::onUserDataClick,
onLinkClick = { url, customTab -> onLinkClick(activity, isDark, url, state.timelineState.eventSink, customTab) },
onSendLocationClick = this::onSendLocationClick,
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 434f6643d4..40a8de1276 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
@@ -48,7 +48,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
-import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
+import io.element.android.features.messages.impl.voicemessages.composer.DefaultVoiceMessageComposerPresenter
import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
@@ -63,6 +63,9 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
+import io.element.android.libraries.featureflag.api.FeatureFlagService
+import io.element.android.libraries.featureflag.api.FeatureFlags
+import io.element.android.libraries.matrix.api.core.toThreadId
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
@@ -93,7 +96,7 @@ class MessagesPresenter @AssistedInject constructor(
@Assisted private val navigator: MessagesNavigator,
private val room: JoinedRoom,
@Assisted private val composerPresenter: Presenter,
- private val voiceMessageComposerPresenter: Presenter,
+ voiceMessageComposerPresenterFactory: DefaultVoiceMessageComposerPresenter.Factory,
@Assisted private val timelinePresenter: Presenter,
private val timelineProtectionPresenter: Presenter,
private val identityChangeStatePresenter: Presenter,
@@ -111,10 +114,11 @@ class MessagesPresenter @AssistedInject constructor(
private val clipboardHelper: ClipboardHelper,
private val htmlConverterProvider: HtmlConverterProvider,
private val buildMeta: BuildMeta,
- private val timelineController: TimelineController,
+ @Assisted private val timelineController: TimelineController,
private val permalinkParser: PermalinkParser,
private val analyticsService: AnalyticsService,
private val encryptionService: EncryptionService,
+ private val featureFlagService: FeatureFlagService,
) : Presenter {
@AssistedFactory
interface Factory {
@@ -123,9 +127,14 @@ class MessagesPresenter @AssistedInject constructor(
composerPresenter: Presenter,
timelinePresenter: Presenter,
actionListPresenter: Presenter,
+ timelineController: TimelineController,
): MessagesPresenter
}
+ private val voiceMessageComposerPresenter = voiceMessageComposerPresenterFactory.create(
+ timelineMode = timelineController.mainTimelineMode()
+ )
+
@Composable
override fun present(): MessagesState {
htmlConverterProvider.Update()
@@ -145,9 +154,8 @@ class MessagesPresenter @AssistedInject constructor(
val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present()
val roomCallState = roomCallStatePresenter.present()
val roomMemberModerationState = roomMemberModerationPresenter.present()
- val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
- val userEventPermissions by userEventPermissions(syncUpdateFlow.value)
+ val userEventPermissions by userEventPermissions(roomInfo)
val roomAvatar by remember {
derivedStateOf { roomInfo.avatarData() }
@@ -264,8 +272,13 @@ class MessagesPresenter @AssistedInject constructor(
}
@Composable
- private fun userEventPermissions(updateKey: Long): State {
- return produceState(UserEventPermissions.DEFAULT, key1 = updateKey) {
+ private fun userEventPermissions(roomInfo: RoomInfo): State {
+ val key = if (roomInfo.privilegedCreatorRole && roomInfo.creators.contains(room.sessionId)) {
+ Long.MAX_VALUE
+ } else {
+ roomInfo.roomPowerLevels?.hashCode() ?: 0L
+ }
+ 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 },
@@ -309,8 +322,17 @@ class MessagesPresenter @AssistedInject constructor(
TimelineItemAction.AddCaption -> handleActionAddCaption(targetEvent, composerState)
TimelineItemAction.EditCaption -> handleActionEditCaption(targetEvent, composerState)
TimelineItemAction.RemoveCaption -> handleRemoveCaption(targetEvent)
- TimelineItemAction.Reply,
- TimelineItemAction.ReplyInThread -> handleActionReply(targetEvent, composerState, timelineProtectionState)
+ TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState, timelineProtectionState)
+ TimelineItemAction.ReplyInThread -> {
+ val displayThreads = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
+ if (displayThreads) {
+ // Get either the thread id this event is in, or the event id if it's not in a thread so we can start one
+ val threadId = targetEvent.threadInfo.threadRootId ?: targetEvent.eventId!!.toThreadId()
+ navigator.onOpenThread(threadId, null)
+ } else {
+ handleActionReply(targetEvent, composerState, timelineProtectionState)
+ }
+ }
TimelineItemAction.ViewSource -> handleShowDebugInfoAction(targetEvent)
TimelineItemAction.Forward -> handleForwardAction(targetEvent)
TimelineItemAction.ReportContent -> handleReportAction(targetEvent)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt
index ddeff51b7e..3d05da5a3c 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt
@@ -8,6 +8,7 @@
package io.element.android.features.messages.impl
import androidx.compose.runtime.Immutable
+import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
import io.element.android.features.messages.impl.link.LinkState
@@ -18,7 +19,6 @@ import io.element.android.features.messages.impl.timeline.components.customreact
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
-import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
import io.element.android.libraries.architecture.AsyncData
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 b45bb781c5..8217cb977c 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
@@ -8,6 +8,9 @@
package io.element.android.features.messages.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
+import io.element.android.features.messages.api.timeline.voicemessages.composer.aVoiceMessageComposerState
+import io.element.android.features.messages.api.timeline.voicemessages.composer.aVoiceMessagePreviewState
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.anActionListState
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
@@ -31,9 +34,6 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
-import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
-import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
-import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState
import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.features.roomcall.api.aStandByCallState
import io.element.android.features.roomcall.api.anOngoingCallState
@@ -43,8 +43,10 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
import kotlinx.collections.immutable.persistentListOf
@@ -84,6 +86,10 @@ open class MessagesStateProvider : PreviewParameterProvider {
aMessagesState(roomName = "A DM with a very looong name", dmUserVerificationState = IdentityState.Verified),
aMessagesState(roomName = "A DM with a very looong name", dmUserVerificationState = IdentityState.VerificationViolation),
aMessagesState(successorRoom = SuccessorRoom(RoomId("!id:domain"), null)),
+ aMessagesState(timelineState = aTimelineState(
+ timelineMode = Timeline.Mode.Thread(threadRootId = ThreadId("\$a-thread-id")),
+ timelineItems = aTimelineItemList(aTimelineItemTextContent()),
+ )),
)
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
index 74f2fb18a5..ccbebbbc6d 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
@@ -48,6 +48,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents
import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListView
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
@@ -73,7 +74,6 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheet
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
import io.element.android.features.messages.impl.timeline.model.TimelineItem
-import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageSendingFailedDialog
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
@@ -105,6 +105,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.textcomposer.model.TextEditorState
import io.element.android.libraries.ui.strings.CommonStrings
@@ -196,17 +197,21 @@ fun MessagesView(
topBar = {
Column {
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
- MessagesViewTopBar(
- roomName = state.roomName,
- roomAvatar = state.roomAvatar,
- isTombstoned = state.isTombstoned,
- heroes = state.heroes,
- roomCallState = state.roomCallState,
- dmUserIdentityState = state.dmUserVerificationState,
- onBackClick = { hidingKeyboard { onBackClick() } },
- onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
- onJoinCallClick = onJoinCallClick,
- )
+ if (state.timelineState.timelineMode is Timeline.Mode.Thread) {
+ ThreadTopBar(onBackClick = onBackClick)
+ } else {
+ MessagesViewTopBar(
+ roomName = state.roomName,
+ roomAvatar = state.roomAvatar,
+ isTombstoned = state.isTombstoned,
+ heroes = state.heroes,
+ roomCallState = state.roomCallState,
+ dmUserIdentityState = state.dmUserVerificationState,
+ onBackClick = { hidingKeyboard { onBackClick() } },
+ onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
+ onJoinCallClick = onJoinCallClick,
+ )
+ }
}
},
content = { padding ->
@@ -414,23 +419,26 @@ private fun MessagesViewContent(
onJoinCallClick = onJoinCallClick,
nestedScrollConnection = scrollBehavior.nestedScrollConnection,
)
- AnimatedVisibility(
- visible = state.pinnedMessagesBannerState is PinnedMessagesBannerState.Visible && scrollBehavior.isVisible,
- enter = expandVertically(),
- exit = shrinkVertically(),
- ) {
- fun focusOnPinnedEvent(eventId: EventId) {
- state.timelineState.eventSink(
- TimelineEvents.FocusOnEvent(eventId = eventId, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds)
+
+ if (state.timelineState.timelineMode !is Timeline.Mode.Thread) {
+ AnimatedVisibility(
+ visible = state.pinnedMessagesBannerState is PinnedMessagesBannerState.Visible && scrollBehavior.isVisible,
+ enter = expandVertically(),
+ exit = shrinkVertically(),
+ ) {
+ fun focusOnPinnedEvent(eventId: EventId) {
+ state.timelineState.eventSink(
+ TimelineEvents.FocusOnEvent(eventId = eventId, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds)
+ )
+ }
+ PinnedMessagesBannerView(
+ state = state.pinnedMessagesBannerState,
+ onClick = ::focusOnPinnedEvent,
+ onViewAllClick = onViewAllPinnedMessagesClick,
)
}
- PinnedMessagesBannerView(
- state = state.pinnedMessagesBannerState,
- onClick = ::focusOnPinnedEvent,
- onViewAllClick = onViewAllPinnedMessagesClick,
- )
+ knockRequestsBannerView()
}
- knockRequestsBannerView()
}
}
}
@@ -540,6 +548,21 @@ private fun MessagesViewTopBar(
)
}
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun ThreadTopBar(
+ onBackClick: () -> Unit,
+) {
+ TopAppBar(
+ navigationIcon = {
+ BackButton(onClick = onBackClick)
+ },
+ title = {
+ Text(stringResource(CommonStrings.common_thread))
+ }
+ )
+}
+
@Composable
private fun RoomAvatarAndNameRow(
roomName: String?,
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 375e2bdefe..ca7a6fa4a3 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
@@ -39,8 +39,11 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.featureflag.api.FeatureFlagService
+import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.BaseRoom
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -51,22 +54,31 @@ import kotlinx.coroutines.launch
interface ActionListPresenter : Presenter {
interface Factory {
- fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter
+ fun create(
+ postProcessor: TimelineItemActionPostProcessor,
+ timelineMode: Timeline.Mode,
+ ): ActionListPresenter
}
}
class DefaultActionListPresenter @AssistedInject constructor(
@Assisted
private val postProcessor: TimelineItemActionPostProcessor,
+ @Assisted
+ private val timelineMode: Timeline.Mode,
private val appPreferencesStore: AppPreferencesStore,
private val room: BaseRoom,
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
private val dateFormatter: DateFormatter,
+ private val featureFlagService: FeatureFlagService,
) : ActionListPresenter {
@AssistedFactory
@ContributesBinding(RoomScope::class)
interface Factory : ActionListPresenter.Factory {
- override fun create(postProcessor: TimelineItemActionPostProcessor): DefaultActionListPresenter
+ override fun create(
+ postProcessor: TimelineItemActionPostProcessor,
+ timelineMode: Timeline.Mode,
+ ): DefaultActionListPresenter
}
private val comparator = TimelineItemActionComparator()
@@ -86,6 +98,8 @@ class DefaultActionListPresenter @AssistedInject constructor(
room.roomInfoFlow.map { it.pinnedEventIds }
}.collectAsState(initial = persistentListOf())
+ val isThreadsEnabled = featureFlagService.isFeatureEnabledFlow(FeatureFlags.Threads).collectAsState(false)
+
fun handleEvents(event: ActionListEvents) {
when (event) {
ActionListEvents.Clear -> target.value = ActionListState.Target.None
@@ -95,6 +109,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
isDeveloperModeEnabled = isDeveloperModeEnabled,
pinnedEventIds = pinnedEventIds,
target = target,
+ isThreadsEnabled = isThreadsEnabled.value,
)
}
}
@@ -110,7 +125,8 @@ class DefaultActionListPresenter @AssistedInject constructor(
usersEventPermissions: UserEventPermissions,
isDeveloperModeEnabled: Boolean,
pinnedEventIds: ImmutableList,
- target: MutableState
+ target: MutableState,
+ isThreadsEnabled: Boolean,
) = launch {
target.value = ActionListState.Target.Loading(timelineItem)
@@ -119,6 +135,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
usersEventPermissions = usersEventPermissions,
isDeveloperModeEnabled = isDeveloperModeEnabled,
isEventPinned = pinnedEventIds.contains(timelineItem.eventId),
+ isThreadsEnabled = isThreadsEnabled,
)
val verifiedUserSendFailure = userSendFailureFactory.create(timelineItem.localSendState)
@@ -146,14 +163,23 @@ class DefaultActionListPresenter @AssistedInject constructor(
usersEventPermissions: UserEventPermissions,
isDeveloperModeEnabled: Boolean,
isEventPinned: Boolean,
+ isThreadsEnabled: Boolean,
): List {
val canRedact = timelineItem.isMine && usersEventPermissions.canRedactOwn || !timelineItem.isMine && usersEventPermissions.canRedactOther
return buildSet {
if (timelineItem.canBeRepliedTo && usersEventPermissions.canSendMessage) {
- if (timelineItem.isThreaded) {
+ if (isThreadsEnabled && timelineMode !is Timeline.Mode.Thread && timelineItem.isRemote) {
+ // If threads are enabled, we can reply in thread if the item is remote
add(TimelineItemAction.ReplyInThread)
- } else {
add(TimelineItemAction.Reply)
+ } else {
+ if (!isThreadsEnabled && timelineItem.threadInfo.threadRootId != null) {
+ // If threads are not enabled, we can reply in a thread if the item is already in the thread
+ add(TimelineItemAction.ReplyInThread)
+ } else {
+ // Otherwise, we can only reply in the room
+ add(TimelineItemAction.Reply)
+ }
}
}
if (timelineItem.isRemote && timelineItem.content.canBeForwarded()) {
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 720cfacfb5..735d5548e8 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
@@ -20,6 +20,7 @@ import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer
@ContributesNode(RoomScope::class)
@@ -29,7 +30,10 @@ class AttachmentsPreviewNode @AssistedInject constructor(
presenterFactory: AttachmentsPreviewPresenter.Factory,
private val localMediaRenderer: LocalMediaRenderer,
) : Node(buildContext, plugins = plugins) {
- data class Inputs(val attachment: Attachment) : NodeInputs
+ data class Inputs(
+ val attachment: Attachment,
+ val timelineMode: Timeline.Mode,
+ ) : NodeInputs
private val inputs: Inputs = inputs()
@@ -39,6 +43,7 @@ class AttachmentsPreviewNode @AssistedInject constructor(
private val presenter = presenterFactory.create(
attachment = inputs.attachment,
+ timelineMode = inputs.timelineMode,
onDoneListener = onDoneListener,
)
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 45c5c33e11..d01b11a026 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
@@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.attachments.video.MediaOptimizationSelectorPresenter
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
import io.element.android.libraries.androidutils.file.safeDelete
+import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.firstInstanceOf
@@ -32,8 +33,8 @@ import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.core.EventId
-import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
@@ -47,12 +48,12 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import timber.log.Timber
-import kotlin.coroutines.coroutineContext
class AttachmentsPreviewPresenter @AssistedInject constructor(
@Assisted private val attachment: Attachment,
@Assisted private val onDoneListener: OnDoneListener,
- private val mediaSender: MediaSender,
+ @Assisted private val timelineMode: Timeline.Mode,
+ mediaSenderFactory: MediaSender.Factory,
private val permalinkBuilder: PermalinkBuilder,
private val temporaryUriDeleter: TemporaryUriDeleter,
private val mediaOptimizationSelectorPresenterFactory: MediaOptimizationSelectorPresenter.Factory,
@@ -63,10 +64,13 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
interface Factory {
fun create(
attachment: Attachment,
+ timelineMode: Timeline.Mode,
onDoneListener: OnDoneListener,
): AttachmentsPreviewPresenter
}
+ private val mediaSender = mediaSenderFactory.create(timelineMode)
+
@Composable
override fun present(): AttachmentsPreviewState {
val coroutineScope = rememberCoroutineScope()
@@ -258,6 +262,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
mediaOptimizationConfig = mediaOptimizationConfig,
).fold(
onSuccess = { mediaUploadInfo ->
+ Timber.d("Media ${mediaUploadInfo.file.path.orEmpty().hash()} finished processing, it's now ready to upload")
sendActionState.value = SendActionState.Sending.ReadyToUpload(mediaUploadInfo)
},
onFailure = {
@@ -304,20 +309,11 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
dismissAfterSend: Boolean,
inReplyToEventId: EventId?,
) = runCatchingExceptions {
- val context = coroutineContext
- val progressCallback = object : ProgressCallback {
- override fun onProgress(current: Long, total: Long) {
- // Note will not happen if useSendQueue is true
- if (context.isActive) {
- sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat(), mediaUploadInfo)
- }
- }
- }
+ sendActionState.value = SendActionState.Sending.Uploading(mediaUploadInfo)
mediaSender.sendPreProcessedMedia(
mediaUploadInfo = mediaUploadInfo,
caption = caption,
formattedCaption = null,
- progressCallback = progressCallback,
inReplyToEventId = inReplyToEventId,
).getOrThrow()
}.fold(
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt
index 71d29cbfb6..96ff83a8a7 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt
@@ -30,7 +30,7 @@ sealed interface SendActionState {
sealed interface Sending : SendActionState {
data class Processing(val displayProgress: Boolean) : Sending
data class ReadyToUpload(val mediaInfo: MediaUploadInfo) : Sending
- data class Uploading(val progress: Float, val mediaUploadInfo: MediaUploadInfo) : Sending
+ data class Uploading(val mediaUploadInfo: MediaUploadInfo) : Sending
}
data class Failure(val error: Throwable, val mediaUploadInfo: MediaUploadInfo?) : SendActionState
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt
index 43410efa46..ac7034efe7 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt
@@ -39,7 +39,7 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider {
ProgressDialog(
- type = ProgressDialogType.Determinate(sendActionState.progress),
+ type = ProgressDialogType.Indeterminate,
text = stringResource(id = CommonStrings.common_sending),
showCancelButton = true,
onDismissRequest = onDismissClick,
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 b14b17dc01..ea9c3dcc0c 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
@@ -101,8 +101,9 @@ class DefaultMediaOptimizationSelectorPresenter @AssistedInject constructor(
val sizeEstimations = VideoCompressionPreset.entries
.map { preset ->
- val bitRate = preset.compressorHelper().calculateOptimalBitrate(videoDimensions, 30)
- val calculatedSize = (bitRate * duration / 8 * 1.1).roundToLong() // Adding 10% overhead for safety
+ val bitRateAsBytes = preset.compressorHelper().calculateOptimalBitrate(videoDimensions, 30) / 8f
+ val durationInSeconds = duration.inWholeSeconds.toFloat()
+ val calculatedSize = (bitRateAsBytes * durationInSeconds * 1.1f).roundToLong() // Adding 10% overhead for safety
VideoUploadEstimation(
preset = preset,
sizeInBytes = calculatedSize,
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 e8f30043ef..0567bfeffc 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
@@ -18,10 +18,12 @@ import dagger.assisted.AssistedInject
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
interface VideoMetadataExtractor : AutoCloseable {
fun getSize(): Result
- fun getDuration(): Result
+ fun getDuration(): Result
interface Factory {
fun create(uri: Uri): VideoMetadataExtractor
}
@@ -57,9 +59,10 @@ class DefaultVideoMetadataExtractor @AssistedInject constructor(
}
}
- override fun getDuration(): Result = runCatchingExceptions {
+ override fun getDuration(): Result = runCatchingExceptions {
mediaMetadataRetriever.value.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
?.takeIf { it > 0L }
+ ?.milliseconds
?: error("Could not retrieve video duration from metadata")
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesBindsModule.kt
similarity index 90%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt
rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesBindsModule.kt
index ed84ef9df0..5148ea0216 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesBindsModule.kt
@@ -28,14 +28,12 @@ import io.element.android.features.messages.impl.timeline.protection.TimelinePro
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
import io.element.android.features.messages.impl.typing.TypingNotificationPresenter
import io.element.android.features.messages.impl.typing.TypingNotificationState
-import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
-import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.RoomScope
@ContributesTo(RoomScope::class)
@Module
-interface MessagesModule {
+interface MessagesBindsModule {
@Binds
fun bindPinnedMessagesBannerPresenter(presenter: PinnedMessagesBannerPresenter): Presenter
@@ -51,9 +49,6 @@ interface MessagesModule {
@Binds
fun bindLinkPresenter(presenter: LinkPresenter): Presenter
- @Binds
- fun bindVoiceMessageComposerPresenter(presenter: VoiceMessageComposerPresenter): Presenter
-
@Binds
fun bindCustomReactionPresenter(presenter: CustomReactionPresenter): Presenter
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesProvidesModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesProvidesModule.kt
new file mode 100644
index 0000000000..970aa63b75
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesProvidesModule.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.messages.impl.di
+
+import com.squareup.anvil.annotations.ContributesTo
+import dagger.Module
+import dagger.Provides
+import io.element.android.features.messages.impl.timeline.di.LiveTimeline
+import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.matrix.api.room.JoinedRoom
+import io.element.android.libraries.matrix.api.timeline.Timeline
+
+@ContributesTo(RoomScope::class)
+@Module
+object MessagesProvidesModule {
+ @Provides
+ @LiveTimeline
+ fun provideLiveTimeline(joinedRoom: JoinedRoom): Timeline = joinedRoom.liveTimeline
+}
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 8e0cce0e9b..2da723746c 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
@@ -52,11 +52,14 @@ import io.element.android.libraries.matrix.api.room.IntentionalMention
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
+import io.element.android.libraries.matrix.api.room.getDirectRoomMember
import io.element.android.libraries.matrix.api.room.isDm
+import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.timeline.TimelineException
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.map
+import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaSender
@@ -64,6 +67,7 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
+import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState
@@ -97,13 +101,13 @@ import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes
class MessageComposerPresenter @AssistedInject constructor(
@Assisted private val navigator: MessagesNavigator,
- @SessionCoroutineScope
- private val sessionCoroutineScope: CoroutineScope,
+ @Assisted private val timelineController: TimelineController,
+ @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val room: JoinedRoom,
private val mediaPickerProvider: PickerProvider,
private val sessionPreferencesStore: SessionPreferencesStore,
private val localMediaFactory: LocalMediaFactory,
- private val mediaSender: MediaSender,
+ private val mediaSenderFactory: MediaSender.Factory,
private val snackbarDispatcher: SnackbarDispatcher,
private val analyticsService: AnalyticsService,
private val locationService: LocationService,
@@ -113,18 +117,20 @@ class MessageComposerPresenter @AssistedInject constructor(
private val permalinkParser: PermalinkParser,
private val permalinkBuilder: PermalinkBuilder,
permissionsPresenterFactory: PermissionsPresenter.Factory,
- private val timelineController: TimelineController,
private val draftService: ComposerDraftService,
private val mentionSpanProvider: MentionSpanProvider,
private val pillificationHelper: TextPillificationHelper,
private val suggestionsProcessor: SuggestionsProcessor,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
+ private val notificationConversationService: NotificationConversationService,
) : Presenter {
@AssistedFactory
interface Factory {
- fun create(navigator: MessagesNavigator): MessageComposerPresenter
+ fun create(timelineController: TimelineController, navigator: MessagesNavigator): MessageComposerPresenter
}
+ private val mediaSender = mediaSenderFactory.create(timelineMode = timelineController.mainTimelineMode())
+
private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA)
private var pendingEvent: MessageComposerEvents? = null
private val suggestionSearchTrigger = MutableStateFlow(null)
@@ -423,11 +429,13 @@ class MessageComposerPresenter @AssistedInject constructor(
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
when (capturedMode) {
is MessageComposerMode.Attachment,
- is MessageComposerMode.Normal -> room.liveTimeline.sendMessage(
- body = message.markdown,
- htmlBody = message.html,
- intentionalMentions = message.intentionalMentions
- )
+ is MessageComposerMode.Normal -> timelineController.invokeOnCurrentTimeline {
+ sendMessage(
+ body = message.markdown,
+ htmlBody = message.html,
+ intentionalMentions = message.intentionalMentions
+ )
+ }
is MessageComposerMode.Edit -> {
timelineController.invokeOnCurrentTimeline {
// First try to edit the message in the current timeline
@@ -463,6 +471,18 @@ class MessageComposerPresenter @AssistedInject constructor(
}
}
}
+
+ val roomInfo = room.info()
+ val roomMembers = room.membersStateFlow.value
+
+ notificationConversationService.onSendMessage(
+ sessionId = room.sessionId,
+ roomId = roomInfo.id,
+ roomName = roomInfo.name ?: roomInfo.id.value,
+ roomIsDirect = roomInfo.isDm,
+ roomAvatarUrl = roomInfo.avatarUrl ?: roomMembers.getDirectRoomMember(roomInfo = roomInfo, sessionId = room.sessionId)?.avatarUrl,
+ )
+
analyticsService.capture(
Composer(
inThread = capturedMode.inThread,
@@ -510,7 +530,6 @@ class MessageComposerPresenter @AssistedInject constructor(
mediaSender.sendMedia(
uri = uri,
mimeType = mimeType,
- progressCallback = null,
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
).getOrThrow()
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt
index 0cc5bd93ec..7206d2571b 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt
@@ -17,10 +17,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
-import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
-import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
-import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerStateProvider
-import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
+import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents
+import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
+import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerStateProvider
+import io.element.android.features.messages.api.timeline.voicemessages.composer.aVoiceMessageComposerState
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.textcomposer.TextComposer
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 2dbf76bc8d..45bed2cc20 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
@@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
@@ -56,7 +57,10 @@ class PinnedMessagesListNode @AssistedInject constructor(
private val presenter = presenterFactory.create(
navigator = this,
- actionListPresenter = actionListPresenterFactory.create(PinnedMessagesListTimelineActionPostProcessor())
+ actionListPresenter = actionListPresenterFactory.create(
+ postProcessor = PinnedMessagesListTimelineActionPostProcessor(),
+ timelineMode = Timeline.Mode.PinnedEvents,
+ )
)
private val callbacks = plugins()
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 1c9e18c9e6..523c371747 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
@@ -39,6 +39,8 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.di.annotations.SessionCoroutineScope
+import io.element.android.libraries.featureflag.api.FeatureFlagService
+import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.powerlevels.canPinUnpin
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
@@ -71,6 +73,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService,
+ private val featureFlagService: FeatureFlagService,
) : Presenter {
@AssistedFactory
interface Factory {
@@ -115,6 +118,8 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val userEventPermissions by userEventPermissions(syncUpdateFlow.value)
+ val displayThreadSummaries by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Threads).collectAsState(false)
+
var pinnedMessageItems by remember {
mutableStateOf>>(AsyncData.Uninitialized)
}
@@ -134,6 +139,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
timelineRoomInfo = timelineRoomInfo,
timelineProtectionState = timelineProtectionState,
linkState = linkState,
+ displayThreadSummaries = displayThreadSummaries,
userEventPermissions = userEventPermissions,
timelineItems = pinnedMessageItems,
eventSink = ::handleEvents
@@ -230,6 +236,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
private fun pinnedMessagesListState(
timelineRoomInfo: TimelineRoomInfo,
timelineProtectionState: TimelineProtectionState,
+ displayThreadSummaries: Boolean,
linkState: LinkState,
userEventPermissions: UserEventPermissions,
timelineItems: AsyncData>,
@@ -246,6 +253,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
PinnedMessagesListState.Filled(
timelineRoomInfo = timelineRoomInfo,
timelineProtectionState = timelineProtectionState,
+ displayThreadSummaries = displayThreadSummaries,
linkState = linkState,
userEventPermissions = userEventPermissions,
timelineItems = timelineItems.data,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt
index d702e2d40f..8d71899134 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt
@@ -33,6 +33,7 @@ sealed interface PinnedMessagesListState {
val timelineItems: ImmutableList,
val actionListState: ActionListState,
val linkState: LinkState,
+ val displayThreadSummaries: Boolean,
val eventSink: (PinnedMessagesListEvents) -> Unit,
) : PinnedMessagesListState {
val loadedPinnedMessagesCount = timelineItems.count { timelineItem -> timelineItem is TimelineItem.Event }
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt
index 2a9b7a085c..70b457b90c 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt
@@ -92,6 +92,7 @@ fun aLoadedPinnedMessagesListState(
timelineItems: List = emptyList(),
actionListState: ActionListState = anActionListState(),
aUserEventPermissions: UserEventPermissions = UserEventPermissions.DEFAULT,
+ displayThreadSummaries: Boolean = false,
eventSink: (PinnedMessagesListEvents) -> Unit = {}
) = PinnedMessagesListState.Filled(
timelineRoomInfo = timelineRoomInfo,
@@ -100,5 +101,6 @@ fun aLoadedPinnedMessagesListState(
timelineItems = timelineItems.toImmutableList(),
actionListState = actionListState,
userEventPermissions = aUserEventPermissions,
+ displayThreadSummaries = displayThreadSummaries,
eventSink = eventSink,
)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt
index 86cd8c740f..0abfc4d75a 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt
@@ -46,6 +46,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TopAppBar
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.compose.LocalAnalyticsService
@@ -126,6 +127,7 @@ private fun PinnedMessagesListContent(
PinnedMessagesListState.Empty -> PinnedMessagesListEmpty()
is PinnedMessagesListState.Filled -> PinnedMessagesListLoaded(
state = state,
+ displayThreadSummaries = state.displayThreadSummaries,
onEventClick = onEventClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
@@ -163,6 +165,7 @@ private fun PinnedMessagesListEmpty(
@Composable
private fun PinnedMessagesListLoaded(
state: PinnedMessagesListState.Filled,
+ displayThreadSummaries: Boolean,
onEventClick: (event: TimelineItem.Event) -> Unit,
onUserDataClick: (MatrixUser) -> Unit,
onLinkClick: (Link) -> Unit,
@@ -210,6 +213,7 @@ private fun PinnedMessagesListLoaded(
) { timelineItem ->
TimelineItemRow(
timelineItem = timelineItem,
+ timelineMode = Timeline.Mode.PinnedEvents,
timelineRoomInfo = state.timelineRoomInfo,
renderReadReceipts = false,
timelineProtectionState = state.timelineProtectionState,
@@ -222,6 +226,7 @@ private fun PinnedMessagesListLoaded(
onLinkLongClick = onLinkLongClick,
onContentClick = onEventClick,
onLongClick = ::onMessageLongClick,
+ displayThreadSummaries = displayThreadSummaries,
inReplyToClick = {},
onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> },
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
new file mode 100644
index 0000000000..cde141dcd6
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.messages.impl.threads
+
+import android.app.Activity
+import android.content.Context
+import androidx.activity.compose.LocalActivity
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.Lifecycle
+import com.bumble.appyx.core.lifecycle.subscribe
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.bumble.appyx.core.plugin.plugins
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.features.messages.impl.MessagesNavigator
+import io.element.android.features.messages.impl.MessagesPresenter
+import io.element.android.features.messages.impl.MessagesView
+import io.element.android.features.messages.impl.actionlist.ActionListPresenter
+import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
+import io.element.android.features.messages.impl.attachments.Attachment
+import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
+import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
+import io.element.android.features.messages.impl.timeline.TimelineController
+import io.element.android.features.messages.impl.timeline.TimelineEvents
+import io.element.android.features.messages.impl.timeline.TimelinePresenter
+import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
+import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
+import io.element.android.features.messages.impl.timeline.model.TimelineItem
+import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
+import io.element.android.libraries.androidutils.system.openUrlInExternalApp
+import io.element.android.libraries.androidutils.system.toast
+import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.inputs
+import io.element.android.libraries.core.bool.orFalse
+import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
+import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
+import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.ThreadId
+import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.permalink.PermalinkData
+import io.element.android.libraries.matrix.api.permalink.PermalinkParser
+import io.element.android.libraries.matrix.api.room.CreateTimelineParams
+import io.element.android.libraries.matrix.api.room.JoinedRoom
+import io.element.android.libraries.matrix.api.room.alias.matches
+import io.element.android.libraries.matrix.api.timeline.Timeline
+import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
+import io.element.android.libraries.mediaplayer.api.MediaPlayer
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.services.analytics.api.AnalyticsService
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+
+@ContributesNode(RoomScope::class)
+class ThreadedMessagesNode @AssistedInject constructor(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ @ApplicationContext private val context: Context,
+ @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
+ private val room: JoinedRoom,
+ private val analyticsService: AnalyticsService,
+ messageComposerPresenterFactory: MessageComposerPresenter.Factory,
+ timelinePresenterFactory: TimelinePresenter.Factory,
+ presenterFactory: MessagesPresenter.Factory,
+ actionListPresenterFactory: ActionListPresenter.Factory,
+ private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
+ private val mediaPlayer: MediaPlayer,
+ private val permalinkParser: PermalinkParser,
+) : Node(buildContext, plugins = plugins), MessagesNavigator {
+ private val callbacks = plugins()
+
+ data class Inputs(
+ val threadRootEventId: ThreadId,
+ val focusedEventId: EventId?,
+ ) : NodeInputs
+
+ private val inputs = inputs()
+
+ // TODO use a loading state node to preload this instead of using `runBlocking`
+ private val threadedTimeline = runBlocking { room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = inputs.threadRootEventId)).getOrThrow() }
+ private val timelineController = TimelineController(room, threadedTimeline)
+ private val presenter = presenterFactory.create(
+ navigator = this,
+ composerPresenter = messageComposerPresenterFactory.create(timelineController, this),
+ timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this),
+ // TODO add special processor for threaded timeline
+ actionListPresenter = actionListPresenterFactory.create(
+ postProcessor = TimelineItemActionPostProcessor.Default,
+ timelineMode = timelineController.mainTimelineMode(),
+ ),
+ timelineController = timelineController,
+ )
+
+ interface Callback : Plugin {
+ fun onEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean
+ fun onPreviewAttachments(attachments: ImmutableList)
+ fun onUserDataClick(userId: UserId)
+ fun onPermalinkClick(data: PermalinkData)
+ fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo)
+ fun onForwardEventClick(eventId: EventId)
+ fun onReportMessage(eventId: EventId, senderId: UserId)
+ fun onSendLocationClick()
+ fun onCreatePollClick()
+ fun onEditPollClick(eventId: EventId)
+ fun onJoinCallClick(roomId: RoomId)
+ }
+
+ override fun onBuilt() {
+ super.onBuilt()
+ lifecycle.subscribe(
+ onCreate = {
+ sessionCoroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) }
+ },
+ onDestroy = {
+ mediaPlayer.close()
+ }
+ )
+ }
+
+ 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.
+ // - if a callback returns false, the other callback will not be invoked.
+ return callbacks.takeIf { it.isNotEmpty() }
+ ?.map { it.onEventClick(timelineMode, event) }
+ ?.all { it }
+ .orFalse()
+ }
+
+ private fun onUserDataClick(userId: UserId) {
+ callbacks.forEach { it.onUserDataClick(userId) }
+ }
+
+ private fun onLinkClick(
+ activity: Activity,
+ darkTheme: Boolean,
+ url: String,
+ eventSink: (TimelineEvents) -> Unit,
+ customTab: Boolean
+ ) {
+ when (val permalink = permalinkParser.parse(url)) {
+ is PermalinkData.UserLink -> {
+ // Open the room member profile, it will fallback to
+ // the user profile if the user is not in the room
+ callbacks.forEach { it.onUserDataClick(permalink.userId) }
+ }
+ is PermalinkData.RoomLink -> {
+ handleRoomLinkClick(permalink, eventSink)
+ }
+ is PermalinkData.FallbackLink -> {
+ if (customTab) {
+ activity.openUrlInChromeCustomTab(null, darkTheme, url)
+ } else {
+ activity.openUrlInExternalApp(url)
+ }
+ }
+ is PermalinkData.RoomEmailInviteLink -> {
+ activity.openUrlInChromeCustomTab(null, darkTheme, url)
+ }
+ }
+ }
+
+ private fun handleRoomLinkClick(
+ roomLink: PermalinkData.RoomLink,
+ eventSink: (TimelineEvents) -> Unit,
+ ) {
+ if (room.matches(roomLink.roomIdOrAlias)) {
+ val eventId = roomLink.eventId
+ if (eventId != null) {
+ eventSink(TimelineEvents.FocusOnEvent(eventId))
+ } else {
+ // Click on the same room, ignore
+ displaySameRoomToast()
+ }
+ } else {
+ callbacks.forEach { it.onPermalinkClick(roomLink) }
+ }
+ }
+
+ override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
+ callbacks.forEach { it.onShowEventDebugInfoClick(eventId, debugInfo) }
+ }
+
+ override fun onForwardEventClick(eventId: EventId) {
+ callbacks.forEach { it.onForwardEventClick(eventId) }
+ }
+
+ override fun onReportContentClick(eventId: EventId, senderId: UserId) {
+ callbacks.forEach { it.onReportMessage(eventId, senderId) }
+ }
+
+ override fun onEditPollClick(eventId: EventId) {
+ callbacks.forEach { it.onEditPollClick(eventId) }
+ }
+
+ override fun onPreviewAttachment(attachments: ImmutableList) {
+ callbacks.forEach { it.onPreviewAttachments(attachments) }
+ }
+
+ override fun onNavigateToRoom(roomId: RoomId, serverNames: List) = Unit
+
+ private fun onSendLocationClick() {
+ callbacks.forEach { it.onSendLocationClick() }
+ }
+
+ private fun onCreatePollClick() {
+ callbacks.forEach { it.onCreatePollClick() }
+ }
+
+ private fun onJoinCallClick() {
+ 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)
+ val isDark = ElementTheme.isLightTheme.not()
+ CompositionLocalProvider(
+ LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
+ ) {
+ val state = presenter.present()
+ OnLifecycleEvent { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_PAUSE -> state.composerState.eventSink(MessageComposerEvents.SaveDraft)
+ else -> Unit
+ }
+ }
+ MessagesView(
+ state = state,
+ onBackClick = this::navigateUp,
+ onRoomDetailsClick = {},
+ onEventContentClick = { isLive, event ->
+ if (isLive) {
+ onEventClick(timelineController.mainTimelineMode(), event)
+ } else {
+ val detachedTimelineMode = timelineController.detachedTimelineMode()
+ if (detachedTimelineMode != null) {
+ onEventClick(detachedTimelineMode, event)
+ } else {
+ false
+ }
+ }
+ },
+ onUserDataClick = this::onUserDataClick,
+ onLinkClick = { url, customTab ->
+ onLinkClick(
+ activity,
+ isDark,
+ url,
+ state.timelineState.eventSink,
+ customTab
+ )
+ },
+ onSendLocationClick = this::onSendLocationClick,
+ onCreatePollClick = this::onCreatePollClick,
+ onJoinCallClick = this::onJoinCallClick,
+ onViewAllPinnedMessagesClick = {},
+ modifier = modifier,
+ knockRequestsBannerView = {},
+ )
+
+ var focusedEventId by rememberSaveable {
+ mutableStateOf(inputs.focusedEventId)
+ }
+ LaunchedEffect(Unit) {
+ focusedEventId?.also { eventId ->
+ state.timelineState.eventSink(TimelineEvents.FocusOnEvent(eventId))
+ }
+ // Reset the focused event id to null to avoid refocusing when restoring node.
+ focusedEventId = null
+ }
+ }
+ }
+}
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 4800d64c9b..6289feecfd 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
@@ -8,6 +8,7 @@
package io.element.android.features.messages.impl.timeline
import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.messages.impl.timeline.di.LiveTimeline
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.core.EventId
@@ -43,11 +44,12 @@ import javax.inject.Inject
@ContributesBinding(RoomScope::class, boundType = TimelineProvider::class)
class TimelineController @Inject constructor(
private val room: JoinedRoom,
+ @LiveTimeline private val liveTimeline: Timeline,
) : Closeable, TimelineProvider {
private val coroutineScope = CoroutineScope(SupervisorJob())
- private val liveTimeline = flowOf(room.liveTimeline)
- private val detachedTimeline = MutableStateFlow>(Optional.empty())
+ private val liveTimelineFlow = flowOf(liveTimeline)
+ private val detachedTimelineFlow = MutableStateFlow>(Optional.empty())
@OptIn(ExperimentalCoroutinesApi::class)
fun timelineItems(): Flow> {
@@ -55,7 +57,13 @@ class TimelineController @Inject constructor(
}
fun isLive(): Flow {
- return detachedTimeline.map { !it.isPresent }
+ return detachedTimelineFlow.map { !it.isPresent }
+ }
+
+ fun mainTimelineMode(): Timeline.Mode = liveTimeline.mode
+
+ fun detachedTimelineMode(): Timeline.Mode? {
+ return detachedTimelineFlow.value.orElse(null)?.mode
}
suspend fun invokeOnCurrentTimeline(block: suspend (Timeline.() -> Unit)) {
@@ -72,7 +80,7 @@ class TimelineController @Inject constructor(
}
}
.map { newDetachedTimeline ->
- detachedTimeline.getAndUpdate { current ->
+ detachedTimelineFlow.getAndUpdate { current ->
if (current.isPresent) {
current.get().close()
}
@@ -90,7 +98,7 @@ class TimelineController @Inject constructor(
}
private fun closeDetachedTimeline() {
- detachedTimeline.getAndUpdate {
+ detachedTimelineFlow.getAndUpdate {
when {
it.isPresent -> {
it.get().close()
@@ -115,7 +123,7 @@ class TimelineController @Inject constructor(
}
}
- private val currentTimelineFlow = combine(liveTimeline, detachedTimeline) { live, detached ->
+ private val currentTimelineFlow = combine(liveTimelineFlow, detachedTimelineFlow) { live, detached ->
when {
detached.isPresent -> detached.get()
else -> live
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt
index 66d8da771b..0fe4394fa6 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt
@@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.timeline
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import kotlin.time.Duration
@@ -31,6 +32,7 @@ sealed interface TimelineEvents {
data class ComputeVerifiedUserSendFailure(val event: TimelineItem.Event) : EventFromTimelineItem
data class ShowShieldDialog(val messageShield: MessageShield) : EventFromTimelineItem
data class LoadMore(val direction: Timeline.PaginationDirection) : EventFromTimelineItem
+ data class OpenThread(val threadRootEventId: ThreadId, val focusedEvent: EventId?) : EventFromTimelineItem
/**
* Navigate to the predecessor or successor room of the current room.
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 86dc1dfb1e..ef740e3a8b 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
@@ -15,6 +15,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
@@ -39,6 +40,8 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.annotations.SessionCoroutineScope
+import io.element.android.libraries.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.room.JoinedRoom
@@ -46,6 +49,7 @@ import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.timeline.ReceiptType
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
@@ -74,16 +78,20 @@ class TimelinePresenter @AssistedInject constructor(
private val sendPollResponseAction: SendPollResponseAction,
private val endPollAction: EndPollAction,
private val sessionPreferencesStore: SessionPreferencesStore,
- private val timelineController: TimelineController,
+ @Assisted private val timelineController: TimelineController,
private val timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
private val resolveVerifiedUserSendFailurePresenter: Presenter,
private val typingNotificationPresenter: Presenter,
private val roomCallStatePresenter: Presenter,
private val markAsFullyRead: MarkAsFullyRead,
+ private val featureFlagService: FeatureFlagService,
) : Presenter {
@AssistedFactory
interface Factory {
- fun create(navigator: MessagesNavigator): TimelinePresenter
+ fun create(
+ timelineController: TimelineController,
+ navigator: MessagesNavigator
+ ): TimelinePresenter
}
private val timelineItemsFactory: TimelineItemsFactory = timelineItemsFactoryCreator.create(
@@ -97,6 +105,9 @@ class TimelinePresenter @AssistedInject constructor(
@Composable
override fun present(): TimelineState {
val localScope = rememberCoroutineScope()
+
+ val timelineMode = remember { timelineController.mainTimelineMode() }
+
var focusRequestState: FocusRequestState by remember { mutableStateOf(FocusRequestState.None) }
val lastReadReceiptId = rememberSaveable { mutableStateOf(null) }
@@ -124,9 +135,17 @@ class TimelinePresenter @AssistedInject constructor(
timelineController.isLive()
}.collectAsState(initial = true)
+ val displayThreadSummaries by produceState(false) {
+ value = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
+ }
+
fun handleEvents(event: TimelineEvents) {
when (event) {
is TimelineEvents.LoadMore -> {
+ if (event.direction == Timeline.PaginationDirection.FORWARDS && timelineMode is Timeline.Mode.Thread) {
+ // Do not paginate forwards in thread mode, as it's not supported
+ return
+ }
localScope.launch {
timelineController.paginate(direction = event.direction)
}
@@ -148,15 +167,21 @@ class TimelinePresenter @AssistedInject constructor(
}
}
is TimelineEvents.SelectPollAnswer -> sessionCoroutineScope.launch {
- sendPollResponseAction.execute(
- pollStartId = event.pollStartId,
- answerId = event.answerId
- )
+ timelineController.invokeOnCurrentTimeline {
+ sendPollResponseAction.execute(
+ timeline = this,
+ pollStartId = event.pollStartId,
+ answerId = event.answerId
+ )
+ }
}
is TimelineEvents.EndPoll -> sessionCoroutineScope.launch {
- endPollAction.execute(
- pollStartId = event.pollStartId,
- )
+ timelineController.invokeOnCurrentTimeline {
+ endPollAction.execute(
+ timeline = this,
+ pollStartId = event.pollStartId,
+ )
+ }
}
is TimelineEvents.EditPoll -> {
navigator.onEditPollClick(event.pollStartId)
@@ -183,6 +208,12 @@ class TimelinePresenter @AssistedInject constructor(
val serverNames = calculateServerNamesForRoom(room)
navigator.onNavigateToRoom(event.roomId, serverNames)
}
+ is TimelineEvents.OpenThread -> {
+ navigator.onOpenThread(
+ threadRootId = event.threadRootEventId,
+ focusedEventId = event.focusedEvent,
+ )
+ }
}
}
@@ -270,6 +301,7 @@ class TimelinePresenter @AssistedInject constructor(
}
return TimelineState(
timelineItems = timelineItems,
+ timelineMode = timelineMode,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
newEventState = newEventState.value,
@@ -277,6 +309,7 @@ class TimelinePresenter @AssistedInject constructor(
focusRequestState = focusRequestState,
messageShield = messageShield.value,
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
+ displayThreadSummaries = displayThreadSummaries,
eventSink = { handleEvents(it) }
)
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt
index a3cdb34c94..bd1c58c9d7 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt
@@ -16,6 +16,7 @@ import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import kotlinx.collections.immutable.ImmutableList
import kotlin.time.Duration
@@ -24,6 +25,7 @@ import kotlin.time.Duration
data class TimelineState(
val timelineItems: ImmutableList,
val timelineRoomInfo: TimelineRoomInfo,
+ val timelineMode: Timeline.Mode,
val renderReadReceipts: Boolean,
val newEventState: NewEventState,
val isLive: Boolean,
@@ -31,6 +33,7 @@ data class TimelineState(
// If not null, info will be rendered in a dialog
val messageShield: MessageShield?,
val resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState,
+ val displayThreadSummaries: Boolean,
val eventSink: (TimelineEvents) -> Unit,
) {
private val lastTimelineEvent = timelineItems.firstOrNull { it is TimelineItem.Event } as? TimelineItem.Event
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 8bb66b4f60..3c03f4a7b0 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
@@ -31,6 +31,8 @@ import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom
+import io.element.android.libraries.matrix.api.timeline.Timeline
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
@@ -45,12 +47,14 @@ import kotlin.random.Random
fun aTimelineState(
timelineItems: ImmutableList = persistentListOf(),
+ timelineMode: Timeline.Mode = Timeline.Mode.Live,
renderReadReceipts: Boolean = false,
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
focusedEventIndex: Int = -1,
isLive: Boolean = true,
messageShield: MessageShield? = null,
resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState = aResolveVerifiedUserSendFailureState(),
+ displayThreadSummaries: Boolean = false,
eventSink: (TimelineEvents) -> Unit = {},
): TimelineState {
val focusedEventId = timelineItems.filterIsInstance().getOrNull(focusedEventIndex)?.eventId
@@ -61,6 +65,7 @@ fun aTimelineState(
}
return TimelineState(
timelineItems = timelineItems,
+ timelineMode = timelineMode,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
newEventState = NewEventState.None,
@@ -68,6 +73,7 @@ fun aTimelineState(
focusRequestState = focusRequestState,
messageShield = messageShield,
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
+ displayThreadSummaries = displayThreadSummaries,
eventSink = eventSink,
)
}
@@ -140,7 +146,7 @@ internal fun aTimelineItemEvent(
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
sendState: LocalEventSendState? = null,
inReplyTo: InReplyToDetails? = null,
- isThreaded: Boolean = false,
+ threadInfo: EventThreadInfo = EventThreadInfo(threadRootId = null, threadSummary = null),
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(),
readReceiptState: TimelineItemReadReceipts = aTimelineItemReadReceipts(),
@@ -166,7 +172,7 @@ internal fun aTimelineItemEvent(
groupPosition = groupPosition,
localSendState = sendState,
inReplyTo = inReplyTo,
- isThreaded = isThreaded,
+ threadInfo = threadInfo,
origin = null,
timelineItemDebugInfoProvider = { debugInfo },
messageShieldProvider = { messageShield },
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
index 68bbc9c8b9..0d729020dd 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
@@ -163,11 +163,13 @@ fun TimelineView(
) { timelineItem ->
TimelineItemRow(
timelineItem = timelineItem,
+ timelineMode = state.timelineMode,
timelineRoomInfo = state.timelineRoomInfo,
timelineProtectionState = timelineProtectionState,
renderReadReceipts = state.renderReadReceipts,
isLastOutgoingMessage = state.isLastOutgoingMessage(timelineItem.identifier()),
focusedEventId = state.focusedEventId,
+ displayThreadSummaries = state.displayThreadSummaries,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onLinkLongClick = ::onLinkLongClick,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt
index 8434242abd..0ac882a95f 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt
@@ -13,21 +13,26 @@ import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
+import io.element.android.libraries.matrix.api.timeline.Timeline
// For previews
@Composable
internal fun ATimelineItemEventRow(
event: TimelineItem.Event,
+ timelineMode: Timeline.Mode = Timeline.Mode.Live,
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
renderReadReceipts: Boolean = false,
isLastOutgoingMessage: Boolean = false,
timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(),
+ displayThreadSummaries: Boolean = false,
) = TimelineItemEventRow(
event = event,
+ timelineMode = timelineMode,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
timelineProtectionState = timelineProtectionState,
isLastOutgoingMessage = isLastOutgoingMessage,
+ displayThreadSummaries = displayThreadSummaries,
onEventClick = {},
onLongClick = {},
onLinkClick = {},
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt
index 568df86295..96d139bfba 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt
@@ -19,7 +19,7 @@ class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider
get() = sequenceOf(
aTimelineItemEvent(),
- aTimelineItemEvent().copy(localSendState = LocalEventSendState.Sending),
+ aTimelineItemEvent().copy(localSendState = LocalEventSendState.Sending.Event),
aTimelineItemEvent().copy(localSendState = LocalEventSendState.Failed.Unknown("AN_ERROR")),
// Edited
aTimelineItemEvent().copy(content = aTimelineItemTextContent().copy(isEdited = true)),
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
index 1b798de14f..de4cfe2c2a 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
@@ -36,6 +36,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.hideFromAccessibility
@@ -72,6 +73,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
import io.element.android.features.messages.impl.timeline.protection.mustBeProtected
+import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
import io.element.android.libraries.designsystem.components.EqualWidthColumn
import io.element.android.libraries.designsystem.components.avatar.Avatar
@@ -82,10 +84,17 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.swipe.SwipeableActionsState
import io.element.android.libraries.designsystem.swipe.rememberSwipeableActionsState
import io.element.android.libraries.designsystem.text.toPx
+import io.element.android.libraries.designsystem.theme.components.Button
+import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.core.toThreadId
+import io.element.android.libraries.matrix.api.timeline.Timeline
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
+import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName
@@ -97,6 +106,7 @@ 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.testtags.TestTags
import io.element.android.libraries.testtags.testTag
+import io.element.android.libraries.ui.strings.CommonPlurals
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.isTalkbackActive
import io.element.android.wysiwyg.link.Link
@@ -116,10 +126,12 @@ private val BUBBLE_INCOMING_OFFSET = 16.dp
@Composable
fun TimelineItemEventRow(
event: TimelineItem.Event,
+ timelineMode: Timeline.Mode,
timelineRoomInfo: TimelineRoomInfo,
timelineProtectionState: TimelineProtectionState,
renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean,
+ displayThreadSummaries: Boolean,
onEventClick: () -> Unit,
onLongClick: () -> Unit,
onLinkClick: (Link) -> Unit,
@@ -194,6 +206,7 @@ fun TimelineItemEventRow(
}
TimelineItemEventRowContent(
event = event,
+ timelineMode = timelineMode,
timelineProtectionState = timelineProtectionState,
timelineRoomInfo = timelineRoomInfo,
interactionSource = interactionSource,
@@ -227,6 +240,7 @@ fun TimelineItemEventRow(
} else {
TimelineItemEventRowContent(
event = event,
+ timelineMode = timelineMode,
timelineProtectionState = timelineProtectionState,
timelineRoomInfo = timelineRoomInfo,
interactionSource = interactionSource,
@@ -241,6 +255,25 @@ fun TimelineItemEventRow(
eventContentView = eventContentView,
)
}
+
+ if (displayThreadSummaries && timelineMode !is Timeline.Mode.Thread) {
+ event.threadInfo.threadSummary?.let { threadSummary ->
+ val threadPart = stringResource(CommonStrings.common_thread)
+ val numberOfReplies = threadSummary.numberOfReplies.toInt().let { replies ->
+ pluralStringResource(CommonPlurals.common_replies, replies, replies)
+ }
+ Button(
+ modifier = Modifier.padding(horizontal = 24.dp, vertical = 2.dp)
+ .align(if (event.isMine) Alignment.End else Alignment.Start),
+ text = "$threadPart - $numberOfReplies",
+ size = ButtonSize.Small,
+ onClick = {
+ eventSink(TimelineEvents.OpenThread(event.eventId!!.toThreadId(), null))
+ },
+ )
+ }
+ }
+
// Read receipts / Send state
TimelineItemReadReceiptView(
state = ReadReceiptViewState(
@@ -281,6 +314,7 @@ private fun SwipeSensitivity(
@Composable
private fun TimelineItemEventRowContent(
event: TimelineItem.Event,
+ timelineMode: Timeline.Mode,
timelineProtectionState: TimelineProtectionState,
timelineRoomInfo: TimelineRoomInfo,
interactionSource: MutableInteractionSource,
@@ -360,6 +394,7 @@ private fun TimelineItemEventRowContent(
) {
MessageEventBubbleContent(
event = event,
+ timelineMode = timelineMode,
timelineProtectionState = timelineProtectionState,
onMessageLongClick = onLongClick,
inReplyToClick = inReplyToClick,
@@ -461,6 +496,7 @@ private fun MessageSenderInformation(
@Composable
private fun MessageEventBubbleContent(
event: TimelineItem.Event,
+ timelineMode: Timeline.Mode,
timelineProtectionState: TimelineProtectionState,
onMessageLongClick: () -> Unit,
inReplyToClick: () -> Unit,
@@ -658,7 +694,7 @@ private fun MessageEventBubbleContent(
else -> ContentPadding.Textual
}
CommonLayout(
- showThreadDecoration = event.isThreaded,
+ showThreadDecoration = timelineMode !is Timeline.Mode.Thread && event.threadInfo.threadRootId != null,
timestampPosition = timestampPosition,
paddingBehaviour = paddingBehaviour,
inReplyToDetails = event.inReplyTo,
@@ -695,3 +731,28 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
}
}
}
+
+@PreviewsDayNight
+@Composable
+internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview {
+ Column {
+ sequenceOf(false, true).forEach { isMine ->
+ ATimelineItemEventRow(
+ event = aTimelineItemEvent(
+ senderDisplayName = "Sender with a super long name that should ellipsize",
+ isMine = isMine,
+ content = aTimelineItemTextContent(
+ body = "A long text which will be displayed on several lines and" +
+ " hopefully can be manually adjusted to test different behaviors."
+ ),
+ groupPosition = TimelineItemGroupPosition.First,
+ threadInfo = EventThreadInfo(
+ threadRootId = ThreadId("\$thread-root-id"),
+ threadSummary = ThreadSummary(AsyncData.Uninitialized, numberOfReplies = 20L)
+ )
+ ),
+ displayThreadSummaries = true,
+ )
+ }
+ }
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt
index 3de3e2456b..61e86a8d59 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt
@@ -17,6 +17,8 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.matrix.api.core.ThreadId
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsProvider
@@ -56,7 +58,10 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(
),
inReplyTo = inReplyToDetails,
displayNameAmbiguous = displayNameAmbiguous,
- isThreaded = true,
+ threadInfo = EventThreadInfo(
+ threadRootId = ThreadId("\$thread-root-id"),
+ threadSummary = null,
+ ),
groupPosition = TimelineItemGroupPosition.Last,
),
)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt
index a32f36ced6..0ce8e02ecc 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt
@@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.utils.time.isTalkbackActive
import io.element.android.wysiwyg.link.Link
@@ -38,11 +39,13 @@ import io.element.android.wysiwyg.link.Link
@Composable
fun TimelineItemGroupedEventsRow(
timelineItem: TimelineItem.GroupedEvents,
+ timelineMode: Timeline.Mode,
timelineRoomInfo: TimelineRoomInfo,
timelineProtectionState: TimelineProtectionState,
renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean,
focusedEventId: EventId?,
+ displayThreadSummaries: Boolean,
onClick: (TimelineItem.Event) -> Unit,
onLongClick: (TimelineItem.Event) -> Unit,
inReplyToClick: (EventId) -> Unit,
@@ -81,11 +84,13 @@ fun TimelineItemGroupedEventsRow(
isExpanded = isExpanded.value,
onExpandGroupClick = ::onExpandGroupClick,
timelineItem = timelineItem,
+ timelineMode = timelineMode,
timelineRoomInfo = timelineRoomInfo,
timelineProtectionState = timelineProtectionState,
focusedEventId = focusedEventId,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
+ displayThreadSummaries = displayThreadSummaries,
onClick = onClick,
onLongClick = onLongClick,
inReplyToClick = inReplyToClick,
@@ -107,11 +112,13 @@ private fun TimelineItemGroupedEventsRowContent(
isExpanded: Boolean,
onExpandGroupClick: () -> Unit,
timelineItem: TimelineItem.GroupedEvents,
+ timelineMode: Timeline.Mode,
timelineRoomInfo: TimelineRoomInfo,
timelineProtectionState: TimelineProtectionState,
focusedEventId: EventId?,
renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean,
+ displayThreadSummaries: Boolean,
onClick: (TimelineItem.Event) -> Unit,
onLongClick: (TimelineItem.Event) -> Unit,
inReplyToClick: (EventId) -> Unit,
@@ -161,12 +168,14 @@ private fun TimelineItemGroupedEventsRowContent(
}
}.forEach { subGroupEvent ->
TimelineItemRow(
+ timelineMode = timelineMode,
timelineItem = subGroupEvent,
timelineRoomInfo = timelineRoomInfo,
timelineProtectionState = timelineProtectionState,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
focusedEventId = focusedEventId,
+ displayThreadSummaries = displayThreadSummaries,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
@@ -206,11 +215,13 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
isExpanded = true,
onExpandGroupClick = {},
timelineItem = events,
+ timelineMode = Timeline.Mode.Live,
timelineRoomInfo = aTimelineRoomInfo(),
timelineProtectionState = aTimelineProtectionState(),
focusedEventId = events.events.first().eventId,
renderReadReceipts = true,
isLastOutgoingMessage = false,
+ displayThreadSummaries = false,
onClick = {},
onLongClick = {},
onLinkLongClick = {},
@@ -232,11 +243,13 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
isExpanded = false,
onExpandGroupClick = {},
timelineItem = aGroupedEvents(withReadReceipts = true),
+ timelineMode = Timeline.Mode.Live,
timelineRoomInfo = aTimelineRoomInfo(),
timelineProtectionState = aTimelineProtectionState(),
focusedEventId = null,
renderReadReceipts = true,
isLastOutgoingMessage = false,
+ displayThreadSummaries = false,
onClick = {},
onLongClick = {},
onLinkLongClick = {},
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt
index ff318b3f7e..11d7b91e1e 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt
@@ -44,6 +44,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toPx
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.isTalkbackActive
@@ -53,11 +54,13 @@ import kotlin.time.DurationUnit
@Composable
internal fun TimelineItemRow(
timelineItem: TimelineItem,
+ timelineMode: Timeline.Mode,
timelineRoomInfo: TimelineRoomInfo,
renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean,
timelineProtectionState: TimelineProtectionState,
focusedEventId: EventId?,
+ displayThreadSummaries: Boolean,
onUserDataClick: (MatrixUser) -> Unit,
onLinkClick: (Link) -> Unit,
onLinkLongClick: (Link) -> Unit,
@@ -161,10 +164,12 @@ internal fun TimelineItemRow(
}
),
event = timelineItem,
+ timelineMode = timelineMode,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
timelineProtectionState = timelineProtectionState,
isLastOutgoingMessage = isLastOutgoingMessage,
+ displayThreadSummaries = displayThreadSummaries,
onEventClick = { onContentClick(timelineItem) },
onLongClick = { onLongClick(timelineItem) },
onLinkClick = onLinkClick,
@@ -187,11 +192,13 @@ internal fun TimelineItemRow(
is TimelineItem.GroupedEvents -> {
TimelineItemGroupedEventsRow(
timelineItem = timelineItem,
+ timelineMode = timelineMode,
timelineRoomInfo = timelineRoomInfo,
timelineProtectionState = timelineProtectionState,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
focusedEventId = focusedEventId,
+ displayThreadSummaries = displayThreadSummaries,
onClick = onContentClick,
onLongClick = onLongClick,
inReplyToClick = inReplyToClick,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateForTimelineItemEventRowProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateForTimelineItemEventRowProvider.kt
index 22440fd634..d32de08178 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateForTimelineItemEventRowProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateForTimelineItemEventRowProvider.kt
@@ -17,7 +17,7 @@ class ReadReceiptViewStateForTimelineItemEventRowProvider :
override val values: Sequence
get() = sequenceOf(
aReadReceiptViewState(
- sendState = LocalEventSendState.Sending,
+ sendState = LocalEventSendState.Sending.Event,
),
aReadReceiptViewState(
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt
index 770ffe6721..1295abdacc 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt
@@ -20,7 +20,7 @@ class ReadReceiptViewStateProvider : PreviewParameterProvider
get() = sequenceOf(
aReadReceiptViewState(),
- aReadReceiptViewState(sendState = LocalEventSendState.Sending),
+ aReadReceiptViewState(sendState = LocalEventSendState.Sending.Event),
aReadReceiptViewState(sendState = LocalEventSendState.Sent(EventId("\$eventId"))),
aReadReceiptViewState(
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt
index fd6e5cea53..2d783c394c 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt
@@ -76,7 +76,7 @@ fun TimelineItemReadReceiptView(
}
} else {
when (state.sendState) {
- LocalEventSendState.Sending -> {
+ is LocalEventSendState.Sending -> {
ReadReceiptsRow(modifier) {
Icon(
modifier = Modifier.padding(2.dp),
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/LiveTimeline.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/LiveTimeline.kt
new file mode 100644
index 0000000000..40624c9911
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/LiveTimeline.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.messages.impl.timeline.di
+
+import javax.inject.Qualifier
+
+@Retention(AnnotationRetention.RUNTIME)
+@MustBeDocumented
+@Qualifier
+annotation class LiveTimeline
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 f80c4f85cb..eda3f2a3f6 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
@@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
import io.element.android.libraries.matrix.ui.messages.reply.map
@@ -67,7 +68,6 @@ class TimelineItemEventFactory @AssistedInject constructor(
url = senderProfile.getAvatarUrl(),
size = AvatarSize.TimelineSender
)
- currentTimelineItem.event
return TimelineItem.Event(
id = currentTimelineItem.uniqueId,
eventId = currentTimelineItem.eventId,
@@ -86,7 +86,7 @@ class TimelineItemEventFactory @AssistedInject constructor(
readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers),
localSendState = currentTimelineItem.event.localSendState,
inReplyTo = currentTimelineItem.event.inReplyTo()?.map(permalinkParser = permalinkParser),
- isThreaded = currentTimelineItem.event.isThreaded(),
+ threadInfo = currentTimelineItem.event.threadInfo() ?: EventThreadInfo(threadRootId = null, threadSummary = null),
origin = currentTimelineItem.event.origin,
timelineItemDebugInfoProvider = currentTimelineItem.event.timelineItemDebugInfoProvider,
messageShieldProvider = currentTimelineItem.event.messageShieldProvider,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt
index d753acae0d..94e04dd1d8 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt
@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.SendHandle
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
@@ -81,7 +82,7 @@ sealed interface TimelineItem {
val readReceiptState: TimelineItemReadReceipts,
val localSendState: LocalEventSendState?,
val inReplyTo: InReplyToDetails?,
- val isThreaded: Boolean,
+ val threadInfo: EventThreadInfo,
val origin: TimelineItemEventOrigin?,
val timelineItemDebugInfoProvider: TimelineItemDebugInfoProvider,
val messageShieldProvider: MessageShieldProvider,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt
similarity index 90%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt
rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt
index 9b723ff72a..b8ecab747a 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt
@@ -19,10 +19,18 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
+import com.squareup.anvil.annotations.ContributesBinding
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.messages.api.MessageComposerContext
-import io.element.android.libraries.architecture.Presenter
+import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents
+import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerPresenter
+import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
+import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
@@ -40,22 +48,29 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
-import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
-class VoiceMessageComposerPresenter @Inject constructor(
- @SessionCoroutineScope
- private val sessionCoroutineScope: CoroutineScope,
+class DefaultVoiceMessageComposerPresenter @AssistedInject constructor(
+ @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
+ @Assisted private val timelineMode: Timeline.Mode,
private val voiceRecorder: VoiceRecorder,
private val analyticsService: AnalyticsService,
- private val mediaSender: MediaSender,
+ mediaSenderFactory: MediaSender.Factory,
private val player: VoiceMessageComposerPlayer,
private val messageComposerContext: MessageComposerContext,
permissionsPresenterFactory: PermissionsPresenter.Factory
-) : Presenter {
+) : VoiceMessageComposerPresenter {
+ @ContributesBinding(RoomScope::class)
+ @AssistedFactory
+ interface Factory : VoiceMessageComposerPresenter.Factory {
+ override fun create(timelineMode: Timeline.Mode): DefaultVoiceMessageComposerPresenter
+ }
+
private val permissionsPresenter = permissionsPresenterFactory.create(Manifest.permission.RECORD_AUDIO)
+ private val mediaSender = mediaSenderFactory.create(timelineMode)
+
@Composable
override fun present(): VoiceMessageComposerState {
val localCoroutineScope = rememberCoroutineScope()
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 bd5fc09b9e..5aba4e77c5 100644
--- a/features/messages/impl/src/main/res/values-da/translations.xml
+++ b/features/messages/impl/src/main/res/values-da/translations.xml
@@ -9,11 +9,14 @@
"Rejser og steder"
"Symboler"
"Billedtekster er muligvis ikke synlige for personer, der bruger ældre apps."
+ "Tryk for at ændre videokvaliteten i uploadet"
"Filen kunne ikke uploades."
"Det lykkedes ikke at behandle medier til upload. Prøv venligst igen."
"Upload af medier mislykkedes. Prøv igen."
"Den maksimalt tilladte filstørrelse er %1$s ."
"Filen er for stor til at kunne uploades."
+ "Optimér billedkvaliteten"
+ "Behandler…"
"Bloker bruger"
"Marker, hvis du vil skjule alle nuværende og fremtidige beskeder fra denne bruger"
"Denne meddelelse vil blive indberettet til administratoren af din hjemmeserver. De vil ikke være i stand til at læse nogen krypterede meddelelser."
diff --git a/features/messages/impl/src/main/res/values-en-rUS/translations.xml b/features/messages/impl/src/main/res/values-en-rUS/translations.xml
new file mode 100644
index 0000000000..d19cc82f2c
--- /dev/null
+++ b/features/messages/impl/src/main/res/values-en-rUS/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Optimize image quality"
+
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 f6ed132375..3e07759a68 100644
--- a/features/messages/impl/src/main/res/values-et/translations.xml
+++ b/features/messages/impl/src/main/res/values-et/translations.xml
@@ -9,11 +9,14 @@
"Reisimine ja kohad"
"Sümbolid"
"Selgitused ja alapealkirjad ei pruugi olla nähtavad vanemate rakenduste kasutajatele."
+ "Klõpsa üleslaaditava video kvaliteedi muutmiseks"
"Faili üleslaadimine ei õnnestunud."
"Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."
"Meediafaili üleslaadimine ei õnnestunud. Palun proovi uuesti."
"Maksimaalne lubatud failisuurus on %1$s."
"Fail on üleslaadimiseks liiga suur"
+ "Optimeeri pildikvaliteeti"
+ "Töötlen…"
"Blokeeri kasutaja"
"Vali see eelistus, kui sa soovid peita selle kasutaja kõik senised ja tulevased sõnumid"
"Teade selle sõnumi kohta edastatakse sinu koduserveri haldajale. Haldajal ei ole võimalik lugeda krüptitud sõnumite sisu."
diff --git a/features/messages/impl/src/main/res/values-fa/translations.xml b/features/messages/impl/src/main/res/values-fa/translations.xml
index 205907a3b0..1446c3d389 100644
--- a/features/messages/impl/src/main/res/values-fa/translations.xml
+++ b/features/messages/impl/src/main/res/values-fa/translations.xml
@@ -38,5 +38,9 @@
"نمایش کمتر"
"نمایش بیشتر"
"جدید"
+
+ - "%1$dتغییر اتاق"
+ - "%1$dتغییر اتاق"
+
"%1$s و %2$s"
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 c5b80c02f6..76b9ae22ee 100644
--- a/features/messages/impl/src/main/res/values-fr/translations.xml
+++ b/features/messages/impl/src/main/res/values-fr/translations.xml
@@ -9,11 +9,14 @@
"Voyages & lieux"
"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"
"Le fichier n’a pas pu être envoyé."
"Échec du traitement des médias à télécharger, veuillez réessayer."
"É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é."
+ "Optimiser la qualité de l’image"
+ "Traitement en cours…"
"Bloquer l’utilisateur"
"Cochez si vous souhaitez masquer tous les messages actuels et futurs de cet utilisateur."
"Ce message sera signalé à l’administrateur de votre serveur d’accueil. Il ne pourra lire aucun message chiffré."
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 a37858681d..d72afc0078 100644
--- a/features/messages/impl/src/main/res/values-hu/translations.xml
+++ b/features/messages/impl/src/main/res/values-hu/translations.xml
@@ -9,11 +9,14 @@
"Utazás és helyek"
"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"
"A fájl nem tölthető fel."
"Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra."
"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"
+ "Képminőség optimalizációja"
+ "Feldolgozás…"
"Felhasználó letiltása"
"Jelölje be, ha el akarja rejteni az összes jelenlegi és jövőbeli üzenetet ettől a felhasználótól"
"Ez az üzenet jelentve lesz a Matrix-kiszolgáló adminisztrátorának. Nem fogja tudni elolvasni a titkosított üzeneteket."
diff --git a/features/messages/impl/src/main/res/values-sk/translations.xml b/features/messages/impl/src/main/res/values-sk/translations.xml
index 834ad4111a..6fe6ec73ec 100644
--- a/features/messages/impl/src/main/res/values-sk/translations.xml
+++ b/features/messages/impl/src/main/res/values-sk/translations.xml
@@ -9,11 +9,14 @@
"Cestovanie a miesta"
"Symboly"
"Titulky nemusia byť viditeľné pre ľudí používajúcich staršie aplikácie."
+ "Ťuknutím zmeníte kvalitu nahratého videa"
"Súbor sa nepodarilo nahrať."
"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."
"Nepodarilo sa nahrať médiá, skúste to prosím znova."
"Maximálna povolená veľkosť súboru je %1$s."
"Súbor je príliš veľký na nahratie"
+ "Optimalizovať kvalitu obrázku"
+ "Prebieha spracovanie…"
"Zablokovať používateľa"
"Označte, či chcete skryť všetky aktuálne a budúce správy od tohto používateľa"
"Táto správa bude nahlásená správcovi vášho domovského servera. Nebude môcť prečítať žiadne šifrované správy."
diff --git a/features/messages/impl/src/main/res/values-sv/translations.xml b/features/messages/impl/src/main/res/values-sv/translations.xml
index 5ecb88af84..cd5423b64a 100644
--- a/features/messages/impl/src/main/res/values-sv/translations.xml
+++ b/features/messages/impl/src/main/res/values-sv/translations.xml
@@ -9,8 +9,14 @@
"Resor & platser"
"Symboler"
"Bildtexter kanske inte är synliga för personer som använder äldre appar."
+ "Tryck för att ändra videouppladdningskvaliteten"
+ "Filen kunde inte laddas upp."
"Misslyckades att bearbeta media för uppladdning, vänligen pröva igen."
"Misslyckades att ladda upp media, vänligen pröva igen."
+ "Den maximala tillåtna filstorleken är %1$s."
+ "Filen är för stor för att laddas upp"
+ "Optimera bildkvalitet"
+ "Bearbetar …"
"Blockera användare"
"Markera om du vill dölja alla nuvarande och framtida meddelanden från denna användare"
"Det här meddelandet kommer att rapporteras till din hemservers administratör. Denne kommer inte att kunna läsa några krypterade meddelanden."
diff --git a/features/messages/impl/src/main/res/values-uk/translations.xml b/features/messages/impl/src/main/res/values-uk/translations.xml
index f9a5fc6652..90ef76e0f6 100644
--- a/features/messages/impl/src/main/res/values-uk/translations.xml
+++ b/features/messages/impl/src/main/res/values-uk/translations.xml
@@ -9,11 +9,14 @@
"Подорожі та місця"
"Символи"
"Користувачі старих застосунків можуть не бачити підписи."
+ "Натисніть, щоб змінити якість вивантажуваного відео"
"Файл не може бути вивантажено."
"Не вдалося обробити медіафайл для завантаження, спробуйте ще раз."
"Не вдалося завантажити медіафайл, спробуйте ще раз."
"Максимально дозволений розмір файлу — %1$s."
"Файл завеликий для вивантаження"
+ "Оптимізувати якість зображення"
+ "Обробка…"
"Заблокувати користувача"
"Перевірте, чи хочете ви приховати всі поточні та майбутні повідомлення від цього користувача"
"Це повідомлення буде надіслано адміністраторам вашого домашнього сервера. Вони не зможуть прочитати зашифровані повідомлення."
diff --git a/features/messages/impl/src/main/res/values-uz/translations.xml b/features/messages/impl/src/main/res/values-uz/translations.xml
index 9c1b9e0df7..5e740ee6c1 100644
--- a/features/messages/impl/src/main/res/values-uz/translations.xml
+++ b/features/messages/impl/src/main/res/values-uz/translations.xml
@@ -23,6 +23,7 @@
"So\'ro\'vnoma"
"Matnni formatlash"
"Xabarlar tarixi hozirda mavjud emas."
+ "Xabar tarixi ushbu xonada mavjud emas. Xabar tarixini koʻrish uchun ushbu qurilmani tasdiqlang."
"Ularni yana taklif qilmoqchimisiz?"
"Siz bu chatda yolg\'izsiz"
"Har kim"
diff --git a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml
index 5e4caa1cc6..7854d5a0f3 100644
--- a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml
@@ -9,8 +9,11 @@
"旅行與景點"
"標誌"
"使用舊應用程式的使用者可能看不到標題。"
+ "無法上傳檔案。"
"無法處理要上傳的媒體,請再試一次。"
"無法上傳媒體檔案,請稍後再試。"
+ "允許的最大檔案大小為 %1$s。"
+ "檔案太大,無法上傳"
"封鎖使用者"
"檢查您是否要隱藏所有來自此使用者的目前及未來的訊息"
"此訊息將會回報給您的家伺服器管理員。他們將無法讀取任何已加密的訊息。"
@@ -38,12 +41,24 @@
"較少"
"訊息已複製"
"您沒有權限在此聊天室傳送訊息"
+
+ - "%1$d 個成員有 %2$s 個反應"
+
+
+ - "您與 %1$d 個成員有 %2$s 的反應"
+
+ "您反應了 %1$s"
"較少"
"更多"
+ "顯示反應摘要"
"新訊息"
- "%1$d 個聊天室變更"
+ "跳到新聊天室"
+ "此聊天室已被取代,不再活躍"
+ "檢視舊訊息"
+ "此聊天室為另一個聊天是的延續"
- "%1$s、%2$s 和其他 %3$d 個人"
diff --git a/features/messages/impl/src/main/res/values-zh/translations.xml b/features/messages/impl/src/main/res/values-zh/translations.xml
index 74902e353c..2e9710ac84 100644
--- a/features/messages/impl/src/main/res/values-zh/translations.xml
+++ b/features/messages/impl/src/main/res/values-zh/translations.xml
@@ -44,6 +44,7 @@
- "%1$d 个聊天室变化"
+ "该聊天室是其他聊天室的延续"
- "%1$s,%2$s 和其他 %3$d 个人"
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 f6e6fedbc5..c90ee16e47 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
@@ -10,6 +10,7 @@ package io.element.android.features.messages.impl
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.tests.testutils.lambda.lambdaError
@@ -21,7 +22,8 @@ class FakeMessagesNavigator(
private val onReportContentClickLambda: (eventId: EventId, senderId: UserId) -> Unit = { _, _ -> lambdaError() },
private val onEditPollClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() },
private val onPreviewAttachmentLambda: (attachments: ImmutableList) -> Unit = { _ -> lambdaError() },
- private val onNavigateToRoomLambda: (roomId: RoomId, serverNames: List) -> Unit = { _, _ -> lambdaError() }
+ private val onNavigateToRoomLambda: (roomId: RoomId, serverNames: List) -> Unit = { _, _ -> lambdaError() },
+ private val onOpenThreadLambda: (threadRootId: ThreadId, focusedEventId: EventId?) -> Unit = { _, _ -> lambdaError() },
) : MessagesNavigator {
override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
onShowEventDebugInfoClickLambda(eventId, debugInfo)
@@ -46,4 +48,8 @@ class FakeMessagesNavigator(
override fun onNavigateToRoom(roomId: RoomId, serverNames: List) {
onNavigateToRoomLambda(roomId, serverNames)
}
+
+ override fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) {
+ onOpenThreadLambda(threadRootId, focusedEventId)
+ }
}
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 a64fa03e2b..6a1abd4b3f 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
@@ -34,8 +34,8 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
-import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
+import io.element.android.features.messages.test.timeline.voicemessages.composer.FakeDefaultVoiceMessageComposerPresenterFactory
import io.element.android.features.roomcall.api.aStandByCallState
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
@@ -46,9 +46,13 @@ import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
+import io.element.android.libraries.featureflag.api.FeatureFlags
+import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.core.toThreadId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
@@ -56,6 +60,8 @@ import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
+import io.element.android.libraries.matrix.api.timeline.Timeline
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
@@ -66,6 +72,7 @@ import io.element.android.libraries.matrix.test.A_CAPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
+import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.core.aBuildMeta
@@ -908,7 +915,10 @@ class MessagesPresenterTest {
liveTimeline = timeline,
typingNoticeResult = { Result.success(Unit) },
)
- val presenter = createMessagesPresenter(joinedRoom = room, analyticsService = analyticsService)
+ val presenter = createMessagesPresenter(
+ joinedRoom = room,
+ analyticsService = analyticsService,
+ )
presenter.testWithLifecycleOwner {
val messageEvent = aMessageEvent(
content = aTimelineItemTextContent()
@@ -1047,6 +1057,7 @@ class MessagesPresenterTest {
)
val presenter = createMessagesPresenter(
joinedRoom = room,
+ timeline = timeline,
)
presenter.testWithLifecycleOwner {
skipItems(1)
@@ -1153,6 +1164,74 @@ class MessagesPresenterTest {
}
}
+ @Test
+ fun `present - handle action reply in thread for an event in a thread`() = runTest {
+ val openThreadLambda = lambdaRecorder { _: ThreadId, _: EventId? -> }
+ val presenter = createMessagesPresenter(
+ navigator = FakeMessagesNavigator(onOpenThreadLambda = openThreadLambda),
+ featureFlagService = FakeFeatureFlagService(
+ initialState = mapOf(FeatureFlags.Threads.key to true)
+ ),
+ )
+ presenter.testWithLifecycleOwner {
+ val initialState = awaitItem()
+ initialState.eventSink(MessagesEvents.HandleAction(
+ action = TimelineItemAction.ReplyInThread,
+ event = aMessageEvent(threadInfo = EventThreadInfo(A_THREAD_ID, null))
+ ))
+ awaitItem()
+ openThreadLambda.assertions().isCalledOnce().with(value(A_THREAD_ID), value(null))
+ }
+ }
+
+ @Test
+ fun `present - handle action reply in thread to start a new thread`() = runTest {
+ val openThreadLambda = lambdaRecorder { _: ThreadId, _: EventId? -> }
+ val presenter = createMessagesPresenter(
+ navigator = FakeMessagesNavigator(onOpenThreadLambda = openThreadLambda),
+ featureFlagService = FakeFeatureFlagService(
+ initialState = mapOf(FeatureFlags.Threads.key to true)
+ ),
+ )
+ presenter.testWithLifecycleOwner {
+ val initialState = awaitItem()
+ initialState.eventSink(MessagesEvents.HandleAction(
+ action = TimelineItemAction.ReplyInThread,
+ event = aMessageEvent(
+ // The event id will be used as the thread id instead
+ eventId = AN_EVENT_ID,
+ threadInfo = EventThreadInfo(null, null),
+ )
+ ))
+ awaitItem()
+ openThreadLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID.toThreadId()), value(null))
+ }
+ }
+
+ @Test
+ fun `present - handle action reply in a thread with threads disabled`() = runTest {
+ val composerRecorder = EventsRecorder()
+ val presenter = createMessagesPresenter(
+ featureFlagService = FakeFeatureFlagService(
+ initialState = mapOf(FeatureFlags.Threads.key to false)
+ ),
+ messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
+ )
+ presenter.testWithLifecycleOwner {
+ val initialState = awaitItem()
+ initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ReplyInThread, aMessageEvent()))
+ awaitItem()
+ composerRecorder.assertSingle(
+ MessageComposerEvents.SetMode(
+ composerMode = MessageComposerMode.Reply(
+ replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID),
+ hideImage = false,
+ )
+ )
+ )
+ }
+ }
+
private fun TestScope.createMessagesPresenter(
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
joinedRoom: FakeJoinedRoom = FakeJoinedRoom(
@@ -1168,6 +1247,7 @@ class MessagesPresenterTest {
liveTimeline = FakeTimeline(),
typingNoticeResult = { Result.success(Unit) },
),
+ timeline: Timeline = joinedRoom.liveTimeline,
navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
@@ -1183,12 +1263,13 @@ class MessagesPresenterTest {
aRoomMemberModerationState()
},
encryptionService: FakeEncryptionService = FakeEncryptionService(),
+ featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
actionListEventSink: (ActionListEvents) -> Unit = {},
): MessagesPresenter {
return MessagesPresenter(
room = joinedRoom,
composerPresenter = messageComposerPresenter,
- voiceMessageComposerPresenter = { aVoiceMessageComposerState() },
+ voiceMessageComposerPresenterFactory = FakeDefaultVoiceMessageComposerPresenterFactory(backgroundScope),
timelinePresenter = { aTimelineState(eventSink = timelineEventSink) },
timelineProtectionPresenter = { aTimelineProtectionState() },
actionListPresenter = { anActionListState(eventSink = actionListEventSink) },
@@ -1207,10 +1288,11 @@ class MessagesPresenterTest {
buildMeta = aBuildMeta(),
dispatchers = coroutineDispatchers,
htmlConverterProvider = FakeHtmlConverterProvider(),
- timelineController = TimelineController(joinedRoom),
+ timelineController = TimelineController(joinedRoom, timeline),
permalinkParser = permalinkParser,
encryptionService = encryptionService,
analyticsService = analyticsService,
+ featureFlagService = featureFlagService,
)
}
}
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 50dbb2b7ca..964143ac78 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
@@ -27,11 +27,17 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
+import io.element.android.libraries.featureflag.api.FeatureFlags
+import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.room.BaseRoom
+import io.element.android.libraries.matrix.api.timeline.Timeline
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_CAPTION
import io.element.android.libraries.matrix.test.A_MESSAGE
+import io.element.android.libraries.matrix.test.A_THREAD_ID
+import io.element.android.libraries.matrix.test.A_TRANSACTION_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
@@ -192,7 +198,7 @@ class ActionListPresenterTest {
val messageEvent = aMessageEvent(
isMine = false,
isEditable = false,
- isThreaded = true,
+ threadInfo = EventThreadInfo(threadRootId = A_THREAD_ID, threadSummary = null),
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE)
)
initialState.eventSink.invoke(
@@ -426,7 +432,7 @@ class ActionListPresenterTest {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = true,
- isThreaded = true,
+ threadInfo = EventThreadInfo(threadRootId = A_THREAD_ID, threadSummary = null),
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE)
)
initialState.eventSink.invoke(
@@ -1240,11 +1246,222 @@ class ActionListPresenterTest {
assertThat(target.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.ChangedIdentity(userDisplayName = "Alice"))
}
}
+
+ @Test
+ fun `present - compute for threaded timeline with threads enabled`() = runTest {
+ val presenter = createActionListPresenter(
+ isDeveloperModeEnabled = false,
+ timelineMode = Timeline.Mode.Thread(A_THREAD_ID),
+ featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.Threads.key to true)),
+ )
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ val messageEvent = aMessageEvent(
+ isMine = true,
+ isEditable = false,
+ content = aTimelineItemVoiceContent(
+ caption = null,
+ ),
+ threadInfo = EventThreadInfo(A_THREAD_ID, null)
+ )
+ initialState.eventSink.invoke(
+ ActionListEvents.ComputeForMessage(
+ event = messageEvent,
+ userEventPermissions = aUserEventPermissions(
+ canRedactOwn = true,
+ canRedactOther = false,
+ canSendMessage = true,
+ canSendReaction = true,
+ canPinUnpin = true
+ )
+ )
+ )
+ val successState = awaitItem()
+ assertThat(successState.target).isEqualTo(
+ ActionListState.Target.Success(
+ event = messageEvent,
+ sentTimeFull = "0 Full true",
+ displayEmojiReactions = true,
+ verifiedUserSendFailure = VerifiedUserSendFailure.None,
+ actions = persistentListOf(
+ // This is Reply, not ReplyInThread
+ TimelineItemAction.Reply,
+ TimelineItemAction.Forward,
+ TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
+ TimelineItemAction.Redact,
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `present - compute for remote timeline item with threads enabled`() = runTest {
+ val presenter = createActionListPresenter(
+ isDeveloperModeEnabled = false,
+ featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.Threads.key to true)),
+ )
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ val messageEvent = aMessageEvent(
+ eventId = AN_EVENT_ID,
+ isMine = true,
+ isEditable = false,
+ content = aTimelineItemVoiceContent(
+ caption = null,
+ ),
+ )
+
+ assertThat(messageEvent.isRemote).isTrue()
+
+ initialState.eventSink.invoke(
+ ActionListEvents.ComputeForMessage(
+ event = messageEvent,
+ userEventPermissions = aUserEventPermissions(
+ canRedactOwn = true,
+ canRedactOther = false,
+ canSendMessage = true,
+ canSendReaction = true,
+ canPinUnpin = true
+ )
+ )
+ )
+ val successState = awaitItem()
+ assertThat(successState.target).isEqualTo(
+ ActionListState.Target.Success(
+ event = messageEvent,
+ sentTimeFull = "0 Full true",
+ displayEmojiReactions = true,
+ verifiedUserSendFailure = VerifiedUserSendFailure.None,
+ actions = persistentListOf(
+ TimelineItemAction.Reply,
+ TimelineItemAction.ReplyInThread,
+ TimelineItemAction.Forward,
+ TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
+ TimelineItemAction.Redact,
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `present - compute for remote timeline item already in thread with threads enabled`() = runTest {
+ val presenter = createActionListPresenter(
+ isDeveloperModeEnabled = false,
+ featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.Threads.key to true)),
+ )
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ val messageEvent = aMessageEvent(
+ eventId = AN_EVENT_ID,
+ isMine = true,
+ isEditable = false,
+ content = aTimelineItemVoiceContent(
+ caption = null,
+ ),
+ threadInfo = EventThreadInfo(A_THREAD_ID, null),
+ )
+
+ assertThat(messageEvent.isRemote).isTrue()
+
+ initialState.eventSink.invoke(
+ ActionListEvents.ComputeForMessage(
+ event = messageEvent,
+ userEventPermissions = aUserEventPermissions(
+ canRedactOwn = true,
+ canRedactOther = false,
+ canSendMessage = true,
+ canSendReaction = true,
+ canPinUnpin = true
+ )
+ )
+ )
+ val successState = awaitItem()
+ assertThat(successState.target).isEqualTo(
+ ActionListState.Target.Success(
+ event = messageEvent,
+ sentTimeFull = "0 Full true",
+ displayEmojiReactions = true,
+ verifiedUserSendFailure = VerifiedUserSendFailure.None,
+ actions = persistentListOf(
+ TimelineItemAction.Reply,
+ TimelineItemAction.ReplyInThread,
+ TimelineItemAction.Forward,
+ TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
+ TimelineItemAction.Redact,
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `present - compute for local timeline item with threads enabled`() = runTest {
+ val presenter = createActionListPresenter(
+ isDeveloperModeEnabled = false,
+ featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.Threads.key to true)),
+ )
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ val messageEvent = aMessageEvent(
+ eventId = null,
+ transactionId = A_TRANSACTION_ID,
+ isMine = true,
+ isEditable = false,
+ content = aTimelineItemVoiceContent(
+ caption = null,
+ ),
+ )
+
+ assertThat(messageEvent.isRemote).isFalse()
+
+ initialState.eventSink.invoke(
+ ActionListEvents.ComputeForMessage(
+ event = messageEvent,
+ userEventPermissions = aUserEventPermissions(
+ canRedactOwn = true,
+ canRedactOther = false,
+ canSendMessage = true,
+ canSendReaction = true,
+ canPinUnpin = true
+ )
+ )
+ )
+ val successState = awaitItem()
+ assertThat(successState.target).isEqualTo(
+ ActionListState.Target.Success(
+ event = messageEvent,
+ sentTimeFull = "0 Full true",
+ displayEmojiReactions = true,
+ verifiedUserSendFailure = VerifiedUserSendFailure.None,
+ actions = persistentListOf(
+ // Can't reply in thread for local events
+ TimelineItemAction.Reply,
+ TimelineItemAction.Redact,
+ )
+ )
+ )
+ }
+ }
}
private fun createActionListPresenter(
isDeveloperModeEnabled: Boolean,
room: BaseRoom = FakeBaseRoom(),
+ timelineMode: Timeline.Mode = Timeline.Mode.Live,
+ featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
): ActionListPresenter {
val preferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
return DefaultActionListPresenter(
@@ -1253,5 +1470,7 @@ private fun createActionListPresenter(
room = room,
userSendFailureFactory = VerifiedUserSendFailureFactory(room),
dateFormatter = FakeDateFormatter(),
+ timelineMode = timelineMode,
+ featureFlagService = featureFlagService,
)
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt
index 7f8e10c5b1..0f1da066bd 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt
@@ -26,13 +26,13 @@ import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.core.EventId
-import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.libraries.matrix.api.room.JoinedRoom
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.A_CAPTION
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
@@ -58,6 +58,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
+import io.mockk.every
import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CompletableDeferred
@@ -77,7 +78,9 @@ class AttachmentsPreviewPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
- private val mockMediaUrl: Uri = mockk("localMediaUri")
+ private val mockMediaUrl: Uri = mockk("localMediaUri") {
+ every { path } returns "/path/to/media"
+ }
@Test
fun `present - initial state`() = runTest {
@@ -90,17 +93,11 @@ class AttachmentsPreviewPresenterTest {
@Test
fun `present - send media success scenario`() = runTest {
val sendFileResult =
- lambdaRecorder> { _, _, _, _, _, _ ->
+ lambdaRecorder> { _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
val room = FakeJoinedRoom(
- liveTimeline = FakeTimeline(
- progressCallbackValues = listOf(
- Pair(0, 10),
- Pair(5, 10),
- Pair(10, 10)
- ),
- ).apply {
+ liveTimeline = FakeTimeline().apply {
sendFileLambda = sendFileResult
},
)
@@ -120,9 +117,7 @@ class AttachmentsPreviewPresenterTest {
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = true))
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
- assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0f, mediaUploadInfo))
- assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0.5f, mediaUploadInfo))
- assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(1f, mediaUploadInfo))
+ assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(mediaUploadInfo))
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
sendFileResult.assertions().isCalledOnce()
onDoneListener.assertions().isCalledOnce()
@@ -133,7 +128,7 @@ class AttachmentsPreviewPresenterTest {
@Test
fun `present - send media after pre-processing success scenario`() = runTest {
val sendFileResult =
- lambdaRecorder> { _, _, _, _, _, _ ->
+ lambdaRecorder> { _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
val room = FakeJoinedRoom(
@@ -160,6 +155,7 @@ class AttachmentsPreviewPresenterTest {
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
+ assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(mediaUploadInfo))
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
sendFileResult.assertions().isCalledOnce()
onDoneListener.assertions().isCalledOnce()
@@ -170,7 +166,7 @@ class AttachmentsPreviewPresenterTest {
@Test
fun `present - send media before pre-processing success scenario`() = runTest {
val sendFileResult =
- lambdaRecorder> { _, _, _, _, _, _ ->
+ lambdaRecorder> { _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
val room = FakeJoinedRoom(
@@ -197,6 +193,7 @@ class AttachmentsPreviewPresenterTest {
processLatch.complete(Unit)
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = true))
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
+ assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(mediaUploadInfo))
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
sendFileResult.assertions().isCalledOnce()
onDoneListener.assertions().isCalledOnce()
@@ -281,7 +278,7 @@ class AttachmentsPreviewPresenterTest {
@Test
fun `present - send image with caption success scenario`() = runTest {
val sendImageResult =
- lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: EventId? ->
+ lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: EventId? ->
Result.success(FakeMediaUploadHandler())
}
val mediaPreProcessor = FakeMediaPreProcessor().apply {
@@ -307,6 +304,7 @@ class AttachmentsPreviewPresenterTest {
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Sending.ReadyToUpload::class.java)
+ assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Sending.Uploading::class.java)
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
sendImageResult.assertions().isCalledOnce().with(
any(),
@@ -315,7 +313,6 @@ class AttachmentsPreviewPresenterTest {
value(A_CAPTION),
any(),
any(),
- any(),
)
onDoneListener.assertions().isCalledOnce()
}
@@ -324,7 +321,7 @@ class AttachmentsPreviewPresenterTest {
@Test
fun `present - send video with caption success scenario`() = runTest {
val sendVideoResult =
- lambdaRecorder { _: File, _: File?, _: VideoInfo, _: String?, _: String?, _: ProgressCallback?, _: EventId? ->
+ lambdaRecorder { _: File, _: File?, _: VideoInfo, _: String?, _: String?, _: EventId? ->
Result.success(FakeMediaUploadHandler())
}
val mediaPreProcessor = FakeMediaPreProcessor().apply {
@@ -350,6 +347,7 @@ class AttachmentsPreviewPresenterTest {
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Sending.ReadyToUpload::class.java)
+ assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Sending.Uploading::class.java)
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
sendVideoResult.assertions().isCalledOnce().with(
any(),
@@ -358,7 +356,6 @@ class AttachmentsPreviewPresenterTest {
value(A_CAPTION),
any(),
any(),
- any(),
)
onDoneListener.assertions().isCalledOnce()
}
@@ -367,7 +364,7 @@ class AttachmentsPreviewPresenterTest {
@Test
fun `present - send audio with caption success scenario`() = runTest {
val sendAudioResult =
- lambdaRecorder> { _, _, _, _, _, _ ->
+ lambdaRecorder> { _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
val mediaPreProcessor = FakeMediaPreProcessor().apply {
@@ -391,6 +388,7 @@ class AttachmentsPreviewPresenterTest {
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Sending.ReadyToUpload::class.java)
+ assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Sending.Uploading::class.java)
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
sendAudioResult.assertions().isCalledOnce().with(
any(),
@@ -398,7 +396,6 @@ class AttachmentsPreviewPresenterTest {
value(A_CAPTION),
any(),
any(),
- any(),
)
onDoneListener.assertions().isCalledOnce()
}
@@ -408,7 +405,7 @@ class AttachmentsPreviewPresenterTest {
fun `present - send media failure scenario`() = runTest {
val failure = MediaPreProcessor.Failure(null)
val sendFileResult =
- lambdaRecorder> { _, _, _, _, _, _ ->
+ lambdaRecorder> { _, _, _, _, _ ->
Result.failure(failure)
}
val onDoneListenerResult = lambdaRecorder {}
@@ -426,6 +423,7 @@ class AttachmentsPreviewPresenterTest {
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
+ assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(mediaUploadInfo))
// Check that the onDoneListener is called so the screen would be dismissed
onDoneListenerResult.assertions().isCalledOnce()
@@ -445,7 +443,7 @@ class AttachmentsPreviewPresenterTest {
val presenter = createAttachmentsPreviewPresenter(
room = FakeJoinedRoom(
liveTimeline = FakeTimeline().apply {
- sendFileLambda = { _, _, _, _, _, _ ->
+ sendFileLambda = { _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
}
@@ -460,7 +458,9 @@ class AttachmentsPreviewPresenterTest {
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
+ assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(mediaUploadInfo))
initialState.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState)
+ assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
// The sending is cancelled and the state is kept at ReadyToUpload
ensureAllEventsConsumed()
@@ -480,7 +480,7 @@ class AttachmentsPreviewPresenterTest {
localMedia = localMedia,
room = FakeJoinedRoom(
liveTimeline = FakeTimeline().apply {
- sendFileLambda = { _, _, _, _, _, _ ->
+ sendFileLambda = { _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
}
@@ -521,7 +521,7 @@ class AttachmentsPreviewPresenterTest {
localMedia = localMedia,
room = FakeJoinedRoom(
liveTimeline = FakeTimeline().apply {
- sendFileLambda = { _, _, _, _, _, _ ->
+ sendFileLambda = { _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
}
@@ -577,6 +577,7 @@ class AttachmentsPreviewPresenterTest {
uri = mockMediaUrl,
),
room: JoinedRoom = FakeJoinedRoom(),
+ timelineMode: Timeline.Mode = Timeline.Mode.Live,
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(),
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
temporaryUriDeleter: TemporaryUriDeleter = FakeTemporaryUriDeleter(),
@@ -599,14 +600,24 @@ class AttachmentsPreviewPresenterTest {
return AttachmentsPreviewPresenter(
attachment = aMediaAttachment(localMedia),
onDoneListener = onDoneListener,
- mediaSender = MediaSender(mediaPreProcessor, room, {
- MediaOptimizationConfig(compressImages = true, videoCompressionPreset = VideoCompressionPreset.STANDARD)
- }),
+ mediaSenderFactory = object : MediaSender.Factory {
+ override fun create(timelineMode: Timeline.Mode): MediaSender {
+ return MediaSender(
+ preProcessor = mediaPreProcessor,
+ room = room,
+ timelineMode = timelineMode,
+ mediaOptimizationConfigProvider = {
+ MediaOptimizationConfig(compressImages = true, videoCompressionPreset = VideoCompressionPreset.STANDARD)
+ }
+ )
+ }
+ },
permalinkBuilder = permalinkBuilder,
temporaryUriDeleter = temporaryUriDeleter,
sessionCoroutineScope = this,
dispatchers = testCoroutineDispatchers(),
mediaOptimizationSelectorPresenterFactory = mediaOptimizationSelectorPresenterFactory,
+ timelineMode = timelineMode,
)
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/SendActionStateTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/SendActionStateTest.kt
index 10290610dc..7a6dc988a4 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/SendActionStateTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/SendActionStateTest.kt
@@ -17,10 +17,21 @@ class SendActionStateTest {
@Test
fun `mediaUploadInfo() should return the value from Uploading class`() {
val mediaUploadInfo: MediaUploadInfo = aMediaUploadInfo()
- val state: SendActionState = SendActionState.Sending.Uploading(
- progress = 0.5f,
- mediaUploadInfo = aMediaUploadInfo()
- )
+ val state: SendActionState = SendActionState.Sending.Uploading(mediaUploadInfo = aMediaUploadInfo())
+ assertThat(state.mediaUploadInfo()).isEqualTo(mediaUploadInfo)
+ }
+
+ @Test
+ fun `mediaUploadInfo() should return the value from ReadyToUpload class`() {
+ val mediaUploadInfo: MediaUploadInfo = aMediaUploadInfo()
+ val state: SendActionState = SendActionState.Sending.ReadyToUpload(mediaInfo = aMediaUploadInfo())
+ assertThat(state.mediaUploadInfo()).isEqualTo(mediaUploadInfo)
+ }
+
+ @Test
+ fun `mediaUploadInfo() should return the value from Failure class`() {
+ val mediaUploadInfo: MediaUploadInfo = aMediaUploadInfo()
+ val state: SendActionState = SendActionState.Failure(error = IllegalStateException("An error"), mediaUploadInfo = aMediaUploadInfo())
assertThat(state.mediaUploadInfo()).isEqualTo(mediaUploadInfo)
}
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenterTest.kt
index b529918ddc..d095f0d58a 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenterTest.kt
@@ -34,6 +34,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import kotlin.time.Duration.Companion.minutes
@RunWith(AndroidJUnit4::class)
class DefaultMediaOptimizationSelectorPresenterTest {
@@ -158,7 +159,7 @@ class DefaultMediaOptimizationSelectorPresenterTest {
mediaExtractorFactory = FakeVideoMetadataExtractorFactory(
FakeVideoMetadataExtractor(
sizeResult = Result.success(Size(10_000, 10_000)),
- duration = Result.success(600L)
+ duration = Result.success(10.minutes)
)
),
)
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt
index a8dbab2cf2..a9f2e075ac 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt
@@ -19,6 +19,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UniqueId
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShieldProvider
import io.element.android.libraries.matrix.api.timeline.item.event.SendHandleProvider
@@ -40,7 +41,7 @@ internal fun aMessageEvent(
canBeRepliedTo: Boolean = true,
content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, formattedBody = A_MESSAGE, isEdited = false),
inReplyTo: InReplyToDetails? = null,
- isThreaded: Boolean = false,
+ threadInfo: EventThreadInfo = EventThreadInfo(threadRootId = null, threadSummary = null),
sendState: LocalEventSendState = LocalEventSendState.Sent(AN_EVENT_ID),
debugInfoProvider: TimelineItemDebugInfoProvider = TimelineItemDebugInfoProvider { aTimelineItemDebugInfo() },
messageShieldProvider: MessageShieldProvider = MessageShieldProvider { null },
@@ -61,7 +62,7 @@ internal fun aMessageEvent(
readReceiptState = TimelineItemReadReceipts(emptyList().toImmutableList()),
localSendState = sendState,
inReplyTo = inReplyTo,
- isThreaded = isThreaded,
+ threadInfo = threadInfo,
origin = null,
timelineItemDebugInfoProvider = debugInfoProvider,
messageShieldProvider = messageShieldProvider,
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt
index c0df1a2b19..84aeb2ce28 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt
@@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.TimelineException
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
@@ -84,6 +85,7 @@ import io.element.android.libraries.permissions.test.FakePermissionsPresenterFac
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
+import io.element.android.libraries.push.test.notifications.conversations.FakeNotificationConversationService
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
@@ -127,6 +129,7 @@ class MessageComposerPresenterTest {
private val mockMediaUrl: Uri = mockk("localMediaUri")
private val localMediaFactory = FakeLocalMediaFactory(mockMediaUrl)
private val analyticsService = FakeAnalyticsService()
+ private val notificationConversationService = FakeNotificationConversationService()
@Test
fun `present - initial state`() = runTest {
@@ -1181,7 +1184,7 @@ class MessageComposerPresenterTest {
room = FakeJoinedRoom(
typingNoticeResult = { Result.success(Unit) },
liveTimeline = FakeTimeline().apply {
- sendFileLambda = { _, _, _, _, _, _ ->
+ sendFileLambda = { _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
}
@@ -1521,6 +1524,7 @@ class MessageComposerPresenterTest {
room: JoinedRoom = FakeJoinedRoom(
typingNoticeResult = { Result.success(Unit) }
),
+ timeline: Timeline = room.liveTimeline,
navigator: MessagesNavigator = FakeMessagesNavigator(),
pickerProvider: PickerProvider = this@MessageComposerPresenterTest.pickerProvider,
locationService: LocationService = FakeLocationService(true),
@@ -1546,11 +1550,21 @@ class MessageComposerPresenterTest {
mediaPickerProvider = pickerProvider,
sessionPreferencesStore = sessionPreferencesStore,
localMediaFactory = localMediaFactory,
- mediaSender = MediaSender(
- preProcessor = mediaPreProcessor,
- room = room,
- mediaOptimizationConfigProvider = { MediaOptimizationConfig(compressImages = true, videoCompressionPreset = VideoCompressionPreset.STANDARD) }
- ),
+ mediaSenderFactory = object : MediaSender.Factory {
+ override fun create(timelineMode: Timeline.Mode): MediaSender {
+ return MediaSender(
+ preProcessor = mediaPreProcessor,
+ room = room,
+ timelineMode = timelineMode,
+ mediaOptimizationConfigProvider = {
+ MediaOptimizationConfig(
+ compressImages = true,
+ videoCompressionPreset = VideoCompressionPreset.STANDARD
+ )
+ }
+ )
+ }
+ },
snackbarDispatcher = snackbarDispatcher,
analyticsService = analyticsService,
locationService = locationService,
@@ -1560,12 +1574,13 @@ class MessageComposerPresenterTest {
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
permalinkParser = permalinkParser,
permalinkBuilder = permalinkBuilder,
- timelineController = TimelineController(room),
+ timelineController = TimelineController(room, timeline),
draftService = draftService,
mentionSpanProvider = mentionSpanProvider,
pillificationHelper = textPillificationHelper,
suggestionsProcessor = SuggestionsProcessor(),
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
+ notificationConversationService = notificationConversationService,
).apply {
isTesting = true
showTextFormatting = isRichTextEditorEnabled
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt
index 507edf9d9a..07778ab381 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt
@@ -17,6 +17,7 @@ import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProv
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
+import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.sync.SyncService
@@ -297,6 +298,7 @@ class PinnedMessagesListPresenterTest {
room: JoinedRoom = FakeJoinedRoom(),
syncService: SyncService = FakeSyncService(),
analyticsService: AnalyticsService = FakeAnalyticsService(),
+ featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
): PinnedMessagesListPresenter {
val timelineProvider = PinnedEventsTimelineProvider(
room = room,
@@ -314,6 +316,7 @@ class PinnedMessagesListPresenterTest {
actionListPresenter = { anActionListState() },
linkPresenter = { aLinkState() },
analyticsService = analyticsService,
+ featureFlagService = featureFlagService,
sessionCoroutineScope = this,
)
}
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 ab183442f2..a2005b7a39 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
@@ -33,7 +33,7 @@ class TimelineControllerTest {
liveTimeline = liveTimeline,
createTimelineResult = { Result.success(detachedTimeline) }
)
- val sut = TimelineController(joinedRoom)
+ val sut = TimelineController(room = joinedRoom, liveTimeline = liveTimeline)
sut.activeTimelineFlow().test {
awaitItem().also { state ->
@@ -72,7 +72,7 @@ class TimelineControllerTest {
}
}
)
- val sut = TimelineController(joinedRoom)
+ val sut = TimelineController(joinedRoom, liveTimeline)
sut.activeTimelineFlow().test {
awaitItem().also { state ->
@@ -100,7 +100,7 @@ class TimelineControllerTest {
val joinedRoom = FakeJoinedRoom(
liveTimeline = liveTimeline
)
- val sut = TimelineController(joinedRoom)
+ val sut = TimelineController(room = joinedRoom, liveTimeline = liveTimeline)
sut.activeTimelineFlow().test {
awaitItem().also { state ->
assertThat(state).isEqualTo(liveTimeline)
@@ -119,7 +119,7 @@ class TimelineControllerTest {
liveTimeline = liveTimeline,
createTimelineResult = { Result.success(detachedTimeline) }
)
- val sut = TimelineController(joinedRoom)
+ val sut = TimelineController(room = joinedRoom, liveTimeline = liveTimeline)
sut.activeTimelineFlow().test {
awaitItem().also { state ->
assertThat(state).isEqualTo(liveTimeline)
@@ -147,7 +147,7 @@ class TimelineControllerTest {
val joinedRoom = FakeJoinedRoom(
liveTimeline = liveTimeline
)
- val sut = TimelineController(joinedRoom)
+ val sut = TimelineController(room = joinedRoom, liveTimeline = liveTimeline)
assertThat(sut.timelineItems().first()).hasSize(1)
}
@@ -169,7 +169,7 @@ class TimelineControllerTest {
liveTimeline = liveTimeline,
createTimelineResult = { Result.success(detachedTimeline) }
)
- val sut = TimelineController(joinedRoom)
+ val sut = TimelineController(room = joinedRoom, liveTimeline = liveTimeline)
sut.activeTimelineFlow().test {
sut.focusOnEvent(AN_EVENT_ID)
awaitItem().also { state ->
@@ -194,7 +194,7 @@ class TimelineControllerTest {
liveTimeline = liveTimeline,
createTimelineResult = { Result.success(detachedTimeline) }
)
- val sut = TimelineController(joinedRoom)
+ val sut = TimelineController(room = joinedRoom, liveTimeline = liveTimeline)
sut.activeTimelineFlow().test {
awaitItem().also { state ->
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt
index 9ba2688d45..8710af8bda 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
@@ -28,6 +28,7 @@ import io.element.android.features.poll.api.actions.SendPollResponseAction
import io.element.android.features.poll.test.actions.FakeEndPollAction
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
import io.element.android.features.roomcall.api.aStandByCallState
+import io.element.android.libraries.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.UniqueId
@@ -787,6 +788,7 @@ class TimelinePresenterTest {
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead(),
+ featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
): TimelinePresenter {
return TimelinePresenter(
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
@@ -799,11 +801,12 @@ class TimelinePresenterTest {
sendPollResponseAction = sendPollResponseAction,
sessionPreferencesStore = sessionPreferencesStore,
timelineItemIndexer = timelineItemIndexer,
- timelineController = TimelineController(room),
+ timelineController = TimelineController(room, timeline),
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
typingNotificationPresenter = { aTypingNotificationState() },
roomCallStatePresenter = { aStandByCallState() },
markAsFullyRead = markAsFullyRead,
+ featureFlagService = featureFlagService,
)
}
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt
index 10efc72e71..917683220e 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt
@@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.media.ThumbnailInfo
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.permalink.PermalinkData
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
@@ -749,14 +750,14 @@ class TimelineItemContentMessageFactoryTest {
body: String = "Body",
inReplyTo: InReplyTo? = null,
isEdited: Boolean = false,
- isThreaded: Boolean = false,
+ threadInfo: EventThreadInfo = EventThreadInfo(threadRootId = null, threadSummary = null),
type: MessageType,
): MessageContent {
return MessageContent(
body = body,
inReplyTo = inReplyTo,
isEdited = isEdited,
- isThreaded = isThreaded,
+ threadInfo = threadInfo,
type = type,
)
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt
index 0bcd6b8091..188c5f0bcd 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt
@@ -18,6 +18,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
import io.element.android.libraries.matrix.api.core.UniqueId
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID
@@ -41,7 +42,7 @@ class TimelineItemGrouperTest {
isEditable = false,
canBeRepliedTo = false,
inReplyTo = null,
- isThreaded = false,
+ threadInfo = EventThreadInfo(threadRootId = null, threadSummary = null),
origin = null,
timelineItemDebugInfoProvider = { aTimelineItemDebugInfo() },
messageShieldProvider = { null },
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 9f9b8526a1..6040032ebc 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
@@ -17,11 +17,13 @@ import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Composer
+import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents
+import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
import io.element.android.features.messages.impl.messagecomposer.aReplyMode
import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.matrix.api.core.EventId
-import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.media.AudioInfo
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
@@ -64,7 +66,7 @@ class VoiceMessageComposerPresenterTest {
)
private val analyticsService = FakeAnalyticsService()
private val sendVoiceMessageResult =
- lambdaRecorder, ProgressCallback?, EventId?, Result> { _, _, _, _, _ ->
+ lambdaRecorder, EventId?, Result> { _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
private val joinedRoom = FakeJoinedRoom(
@@ -76,6 +78,7 @@ class VoiceMessageComposerPresenterTest {
private val mediaSender = MediaSender(
preProcessor = mediaPreProcessor,
room = joinedRoom,
+ timelineMode = Timeline.Mode.Live,
mediaOptimizationConfigProvider = { MediaOptimizationConfig(compressImages = true, videoCompressionPreset = VideoCompressionPreset.STANDARD) },
)
private val messageComposerContext = FakeMessageComposerContext()
@@ -87,7 +90,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - initial state`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -101,7 +104,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - recording state`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -117,7 +120,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - recording keeps screen on`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -141,7 +144,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - abort recording`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -156,7 +159,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - finish recording`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -173,7 +176,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - play recording before it is ready`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -192,7 +195,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - play recording`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -210,7 +213,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - pause recording`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -229,7 +232,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - seek recording`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -256,7 +259,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - delete recording`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -274,7 +277,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - delete while playing`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -296,7 +299,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - send recording`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -315,7 +318,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - sending is tracked`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -344,7 +347,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - send while playing`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -366,7 +369,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - send recording before previous completed, waits`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -391,7 +394,7 @@ class VoiceMessageComposerPresenterTest {
fun `present - send failures aren't tracked`() = runTest {
// Let sending fail due to media preprocessing error
mediaPreProcessor.givenResult(Result.failure(Exception()))
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -415,7 +418,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - send failures can be retried`() = runTest {
// Let sending fail due to media preprocessing error
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -444,7 +447,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - send failures are displayed as an error dialog`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -479,7 +482,7 @@ class VoiceMessageComposerPresenterTest {
@Test
fun `present - send error - missing recording is tracked`() = runTest {
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -500,7 +503,7 @@ class VoiceMessageComposerPresenterTest {
fun `present - record error - security exceptions are tracked`() = runTest {
val exception = SecurityException("")
voiceRecorder.givenThrowsSecurityException(exception)
- val presenter = createVoiceMessageComposerPresenter()
+ val presenter = createDefaultVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -522,7 +525,7 @@ class VoiceMessageComposerPresenterTest {
val permissionsPresenter = createFakePermissionsPresenter(
recordPermissionGranted = false,
)
- val presenter = createVoiceMessageComposerPresenter(
+ val presenter = createDefaultVoiceMessageComposerPresenter(
permissionsPresenter = permissionsPresenter,
)
moleculeFlow(RecompositionMode.Immediate) {
@@ -551,7 +554,7 @@ class VoiceMessageComposerPresenterTest {
val permissionsPresenter = createFakePermissionsPresenter(
recordPermissionGranted = false,
)
- val presenter = createVoiceMessageComposerPresenter(
+ val presenter = createDefaultVoiceMessageComposerPresenter(
permissionsPresenter = permissionsPresenter,
)
moleculeFlow(RecompositionMode.Immediate) {
@@ -585,7 +588,7 @@ class VoiceMessageComposerPresenterTest {
val permissionsPresenter = createFakePermissionsPresenter(
recordPermissionGranted = false,
)
- val presenter = createVoiceMessageComposerPresenter(
+ val presenter = createDefaultVoiceMessageComposerPresenter(
permissionsPresenter = permissionsPresenter,
)
moleculeFlow(RecompositionMode.Immediate) {
@@ -657,17 +660,22 @@ class VoiceMessageComposerPresenterTest {
}
}
- private fun TestScope.createVoiceMessageComposerPresenter(
+ private fun TestScope.createDefaultVoiceMessageComposerPresenter(
permissionsPresenter: PermissionsPresenter = createFakePermissionsPresenter(),
- ): VoiceMessageComposerPresenter {
- return VoiceMessageComposerPresenter(
- backgroundScope,
- voiceRecorder,
- analyticsService,
- mediaSender,
+ ): DefaultVoiceMessageComposerPresenter {
+ return DefaultVoiceMessageComposerPresenter(
+ sessionCoroutineScope = backgroundScope,
+ timelineMode = Timeline.Mode.Live,
+ voiceRecorder = voiceRecorder,
+ analyticsService = analyticsService,
+ mediaSenderFactory = object : MediaSender.Factory {
+ override fun create(timelineMode: Timeline.Mode): MediaSender {
+ return mediaSender
+ }
+ },
player = VoiceMessageComposerPlayer(FakeMediaPlayer(), this),
messageComposerContext = messageComposerContext,
- FakePermissionsPresenterFactory(permissionsPresenter),
+ permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
)
}
diff --git a/features/messages/test/build.gradle.kts b/features/messages/test/build.gradle.kts
index 5e98674f4e..93f5166f29 100644
--- a/features/messages/test/build.gradle.kts
+++ b/features/messages/test/build.gradle.kts
@@ -15,7 +15,12 @@ android {
dependencies {
api(projects.features.messages.impl)
- implementation(projects.libraries.matrix.api)
+ implementation(projects.libraries.matrix.test)
+ implementation(projects.libraries.mediaplayer.test)
+ implementation(projects.libraries.mediaupload.test)
implementation(projects.libraries.mediaviewer.api)
+ implementation(projects.libraries.permissions.test)
implementation(projects.libraries.preferences.api)
+ implementation(projects.libraries.voicerecorder.test)
+ implementation(projects.services.analytics.test)
}
diff --git a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/attachments/video/FakeVideoMetadataExtractor.kt b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/attachments/video/FakeVideoMetadataExtractor.kt
index 54b27ac675..45d3e31dfb 100644
--- a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/attachments/video/FakeVideoMetadataExtractor.kt
+++ b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/attachments/video/FakeVideoMetadataExtractor.kt
@@ -10,14 +10,16 @@ package io.element.android.features.messages.test.attachments.video
import android.net.Uri
import android.util.Size
import io.element.android.features.messages.impl.attachments.video.VideoMetadataExtractor
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
class FakeVideoMetadataExtractor(
private val sizeResult: Result = Result.success(Size(1, 1)),
- private val duration: Result = Result.success(1L),
+ private val duration: Result = Result.success(1.milliseconds),
) : VideoMetadataExtractor {
override fun getSize(): Result = sizeResult
- override fun getDuration(): Result = duration
+ override fun getDuration(): Result = duration
override fun close() = Unit
}
diff --git a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/voicemessages/composer/FakeDefaultVoiceMessageComposerPresenterFactory.kt b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/voicemessages/composer/FakeDefaultVoiceMessageComposerPresenterFactory.kt
new file mode 100644
index 0000000000..da83fa69e2
--- /dev/null
+++ b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/voicemessages/composer/FakeDefaultVoiceMessageComposerPresenterFactory.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.messages.test.timeline.voicemessages.composer
+
+import io.element.android.features.messages.impl.voicemessages.composer.DefaultVoiceMessageComposerPresenter
+import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer
+import io.element.android.features.messages.test.FakeMessageComposerContext
+import io.element.android.libraries.matrix.api.timeline.Timeline
+import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
+import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
+import io.element.android.libraries.mediaupload.api.MediaSender
+import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider
+import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
+import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
+import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
+import io.element.android.services.analytics.test.FakeAnalyticsService
+import kotlinx.coroutines.CoroutineScope
+
+class FakeDefaultVoiceMessageComposerPresenterFactory(
+ private val sessionCoroutineScope: CoroutineScope,
+ private val mediaSender: MediaSender = MediaSender(
+ preProcessor = FakeMediaPreProcessor(),
+ room = FakeJoinedRoom(),
+ timelineMode = Timeline.Mode.Live,
+ mediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
+ ),
+) : DefaultVoiceMessageComposerPresenter.Factory {
+ override fun create(timelineMode: Timeline.Mode): DefaultVoiceMessageComposerPresenter {
+ return DefaultVoiceMessageComposerPresenter(
+ sessionCoroutineScope = sessionCoroutineScope,
+ timelineMode = timelineMode,
+ voiceRecorder = FakeVoiceRecorder(),
+ analyticsService = FakeAnalyticsService(),
+ mediaSenderFactory = object : MediaSender.Factory {
+ override fun create(timelineMode: Timeline.Mode): MediaSender {
+ return mediaSender
+ }
+ },
+ player = VoiceMessageComposerPlayer(
+ mediaPlayer = FakeMediaPlayer(),
+ sessionCoroutineScope = sessionCoroutineScope,
+ ),
+ messageComposerContext = FakeMessageComposerContext(),
+ permissionsPresenterFactory = FakePermissionsPresenterFactory(),
+ )
+ }
+}
diff --git a/features/migration/impl/build.gradle.kts b/features/migration/impl/build.gradle.kts
index 878cf40d3a..12bf396e74 100644
--- a/features/migration/impl/build.gradle.kts
+++ b/features/migration/impl/build.gradle.kts
@@ -20,6 +20,7 @@ setupAnvil()
dependencies {
implementation(projects.features.migration.api)
implementation(projects.libraries.architecture)
+ implementation(projects.libraries.androidutils)
implementation(projects.libraries.preferences.impl)
implementation(libs.androidx.datastore.preferences)
implementation(projects.features.rageshake.api)
diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt
index a79a073022..61a9ba69a4 100644
--- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt
+++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt
@@ -7,27 +7,22 @@
package io.element.android.features.migration.impl
-import android.content.Context
-import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
-import androidx.datastore.preferences.preferencesDataStore
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
-private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_migration")
private val applicationMigrationVersion = intPreferencesKey("applicationMigrationVersion")
@ContributesBinding(AppScope::class)
class DefaultMigrationStore @Inject constructor(
- @ApplicationContext context: Context,
+ preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : MigrationStore {
- private val store = context.dataStore
+ private val store = preferenceDataStoreFactory.create("elementx_migration")
override suspend fun setApplicationMigrationVersion(version: Int) {
store.edit { prefs ->
diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/EndPollAction.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/EndPollAction.kt
index 127719bf27..8e5b5c6ba3 100644
--- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/EndPollAction.kt
+++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/EndPollAction.kt
@@ -8,7 +8,8 @@
package io.element.android.features.poll.api.actions
import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.timeline.Timeline
interface EndPollAction {
- suspend fun execute(pollStartId: EventId): Result
+ suspend fun execute(timeline: Timeline, pollStartId: EventId): Result
}
diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/SendPollResponseAction.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/SendPollResponseAction.kt
index 9b1e403e7f..49da04829b 100644
--- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/SendPollResponseAction.kt
+++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/SendPollResponseAction.kt
@@ -8,7 +8,12 @@
package io.element.android.features.poll.api.actions
import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.timeline.Timeline
interface SendPollResponseAction {
- suspend fun execute(pollStartId: EventId, answerId: String): Result
+ suspend fun execute(
+ timeline: Timeline,
+ pollStartId: EventId,
+ answerId: String
+ ): Result
}
diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollEntryPoint.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollEntryPoint.kt
index 7b508511ed..348f2f6c60 100644
--- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollEntryPoint.kt
+++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollEntryPoint.kt
@@ -10,9 +10,11 @@ package io.element.android.features.poll.api.create
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.libraries.architecture.FeatureEntryPoint
+import io.element.android.libraries.matrix.api.timeline.Timeline
interface CreatePollEntryPoint : FeatureEntryPoint {
data class Params(
+ val timelineMode: Timeline.Mode,
val mode: CreatePollMode,
)
diff --git a/features/poll/api/src/main/res/values-eu/translations.xml b/features/poll/api/src/main/res/values-eu/translations.xml
new file mode 100644
index 0000000000..07f2180cc2
--- /dev/null
+++ b/features/poll/api/src/main/res/values-eu/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Aurreko hautaketa kenduko du"
+ "Erantzun hau gailendu da"
+
diff --git a/features/poll/api/src/main/res/values-nb/translations.xml b/features/poll/api/src/main/res/values-nb/translations.xml
index 332f319e7c..8b81fd3586 100644
--- a/features/poll/api/src/main/res/values-nb/translations.xml
+++ b/features/poll/api/src/main/res/values-nb/translations.xml
@@ -4,5 +4,6 @@
- "%1$d prosent av totalt antall stemmer"
- "%1$d prosent av totalt antall stemmer"
+ "Vil fjerne forrige valg"
"Dette er vinnersvaret"
diff --git a/features/poll/api/src/main/res/values-nl/translations.xml b/features/poll/api/src/main/res/values-nl/translations.xml
new file mode 100644
index 0000000000..0bc6a6ebf2
--- /dev/null
+++ b/features/poll/api/src/main/res/values-nl/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Verwijdert de vorige selectie"
+
diff --git a/features/poll/api/src/main/res/values-sv/translations.xml b/features/poll/api/src/main/res/values-sv/translations.xml
index 54b7f62d01..e04a499624 100644
--- a/features/poll/api/src/main/res/values-sv/translations.xml
+++ b/features/poll/api/src/main/res/values-sv/translations.xml
@@ -4,5 +4,6 @@
- "%1$d procent av totala röster"
- "%1$d procent av totala röster"
+ "Kommer att ta bort föregående val"
"Detta är det vinnande svaret"
diff --git a/features/poll/api/src/main/res/values-uz/translations.xml b/features/poll/api/src/main/res/values-uz/translations.xml
new file mode 100644
index 0000000000..6e1a16a013
--- /dev/null
+++ b/features/poll/api/src/main/res/values-uz/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Oldingi tanlov olib tashlanadi"
+ "Bu g\'alaba qozongan javob"
+
diff --git a/features/poll/api/src/main/res/values-zh-rTW/translations.xml b/features/poll/api/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..da6b7eb524
--- /dev/null
+++ b/features/poll/api/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,8 @@
+
+
+
+ - "總票數的百分之 %1$d"
+
+ "將會移除先前的選擇"
+ "這是得票數最高的選項"
+
diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultEndPollAction.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultEndPollAction.kt
index ea11ef918e..1bb0d87405 100644
--- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultEndPollAction.kt
+++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultEndPollAction.kt
@@ -12,17 +12,16 @@ import im.vector.app.features.analytics.plan.PollEnd
import io.element.android.features.poll.api.actions.EndPollAction
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
-import io.element.android.libraries.matrix.api.room.JoinedRoom
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.services.analytics.api.AnalyticsService
import javax.inject.Inject
@ContributesBinding(RoomScope::class)
class DefaultEndPollAction @Inject constructor(
- private val room: JoinedRoom,
private val analyticsService: AnalyticsService,
) : EndPollAction {
- override suspend fun execute(pollStartId: EventId): Result {
- return room.liveTimeline.endPoll(
+ override suspend fun execute(timeline: Timeline, pollStartId: EventId): Result {
+ return timeline.endPoll(
pollStartId = pollStartId,
text = "The poll with event id: $pollStartId has ended."
).onSuccess {
diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultSendPollResponseAction.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultSendPollResponseAction.kt
index 4f1f29df46..757fe1803e 100644
--- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultSendPollResponseAction.kt
+++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultSendPollResponseAction.kt
@@ -12,17 +12,16 @@ import im.vector.app.features.analytics.plan.PollVote
import io.element.android.features.poll.api.actions.SendPollResponseAction
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
-import io.element.android.libraries.matrix.api.room.JoinedRoom
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.services.analytics.api.AnalyticsService
import javax.inject.Inject
@ContributesBinding(RoomScope::class)
class DefaultSendPollResponseAction @Inject constructor(
- private val room: JoinedRoom,
private val analyticsService: AnalyticsService,
) : SendPollResponseAction {
- override suspend fun execute(pollStartId: EventId, answerId: String): Result {
- return room.liveTimeline.sendPollResponse(
+ override suspend fun execute(timeline: Timeline, pollStartId: EventId, answerId: String): Result {
+ return timeline.sendPollResponse(
pollStartId = pollStartId,
answers = listOf(answerId),
).onSuccess {
diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt
index 5a86e476a9..1a397b96e6 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
@@ -21,6 +21,7 @@ import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.services.analytics.api.AnalyticsService
import java.util.concurrent.atomic.AtomicBoolean
@@ -31,7 +32,7 @@ class CreatePollNode @AssistedInject constructor(
presenterFactory: CreatePollPresenter.Factory,
analyticsService: AnalyticsService,
) : Node(buildContext, plugins = plugins) {
- data class Inputs(val mode: CreatePollMode) : NodeInputs
+ data class Inputs(val mode: CreatePollMode, val timelineMode: Timeline.Mode) : NodeInputs
private val inputs: Inputs = inputs()
@@ -44,6 +45,7 @@ class CreatePollNode @AssistedInject constructor(
}
},
mode = inputs.mode,
+ timelineMode = inputs.timelineMode,
)
init {
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 f1d9b780ee..f9f8e59ea8 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
@@ -29,6 +29,7 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.poll.PollAnswer
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.poll.isDisclosed
+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
@@ -37,17 +38,24 @@ import kotlinx.coroutines.launch
import timber.log.Timber
class CreatePollPresenter @AssistedInject constructor(
- private val repository: PollRepository,
+ repositoryFactory: PollRepository.Factory,
private val analyticsService: AnalyticsService,
private val messageComposerContext: MessageComposerContext,
@Assisted private val navigateUp: () -> Unit,
@Assisted private val mode: CreatePollMode,
+ @Assisted private val timelineMode: Timeline.Mode,
) : Presenter {
@AssistedFactory
interface Factory {
- fun create(backNavigator: () -> Unit, mode: CreatePollMode): CreatePollPresenter
+ fun create(
+ timelineMode: Timeline.Mode,
+ backNavigator: () -> Unit,
+ mode: CreatePollMode
+ ): CreatePollPresenter
}
+ private val repository = repositoryFactory.create(timelineMode)
+
@Composable
override fun present(): CreatePollState {
// The initial state of the form. In edit mode this will be populated with the poll being edited.
diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt
index fd0e670fc3..019da10de9 100644
--- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt
+++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt
@@ -23,7 +23,7 @@ class DefaultCreatePollEntryPoint @Inject constructor() : CreatePollEntryPoint {
return object : CreatePollEntryPoint.NodeBuilder {
override fun params(params: CreatePollEntryPoint.Params): CreatePollEntryPoint.NodeBuilder {
- plugins += CreatePollNode.Inputs(mode = params.mode)
+ plugins += CreatePollNode.Inputs(timelineMode = params.timelineMode, mode = params.mode)
return this
}
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 0fbdcdee36..ad73b0583f 100644
--- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt
+++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt
@@ -7,24 +7,40 @@
package io.element.android.features.poll.impl.data
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.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
import io.element.android.libraries.matrix.api.poll.PollKind
+import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import io.element.android.libraries.matrix.api.timeline.getActiveTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
-import javax.inject.Inject
-class PollRepository @Inject constructor(
+class PollRepository @AssistedInject constructor(
private val room: JoinedRoom,
- private val timelineProvider: TimelineProvider,
+ private val defaultTimelineProvider: TimelineProvider,
+ @Assisted private val timelineMode: Timeline.Mode,
) {
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ timelineMode: Timeline.Mode,
+ ): PollRepository
+ }
+
suspend fun getPoll(eventId: EventId): Result = runCatchingExceptions {
- timelineProvider
+ getTimelineProvider()
+ .getOrThrow()
.getActiveTimeline()
.timelineItems
.first()
@@ -42,30 +58,51 @@ class PollRepository @Inject constructor(
pollKind: PollKind,
maxSelections: Int,
): Result = when (existingPollId) {
- null -> room.liveTimeline.createPoll(
- question = question,
- answers = answers,
- maxSelections = maxSelections,
- pollKind = pollKind,
- )
- else -> timelineProvider
- .getActiveTimeline()
- .editPoll(
- pollStartId = existingPollId,
- question = question,
- answers = answers,
- maxSelections = maxSelections,
- pollKind = pollKind,
- )
+ null -> getTimelineProvider().flatMap { timelineProvider ->
+ timelineProvider
+ .getActiveTimeline()
+ .createPoll(
+ question = question,
+ answers = answers,
+ maxSelections = maxSelections,
+ pollKind = pollKind,
+ )
+ }
+ else -> getTimelineProvider().flatMap { timelineProvider ->
+ timelineProvider.getActiveTimeline()
+ .editPoll(
+ pollStartId = existingPollId,
+ question = question,
+ answers = answers,
+ maxSelections = maxSelections,
+ pollKind = pollKind,
+ )
+ }
}
suspend fun deletePoll(
pollStartId: EventId,
): Result =
- timelineProvider
- .getActiveTimeline()
- .redactEvent(
- eventOrTransactionId = pollStartId.toEventOrTransactionId(),
- reason = null,
- )
+ getTimelineProvider().flatMap { timelineProvider ->
+ timelineProvider.getActiveTimeline()
+ .redactEvent(
+ eventOrTransactionId = pollStartId.toEventOrTransactionId(),
+ reason = null,
+ )
+ }
+
+ private suspend fun getTimelineProvider(): Result {
+ return when (timelineMode) {
+ is Timeline.Mode.Thread -> {
+ val threadedTimelineResult = room.createTimeline(CreateTimelineParams.Threaded(timelineMode.threadRootId))
+ threadedTimelineResult.map { threadedTimeline ->
+ object : TimelineProvider {
+ private val flow = MutableStateFlow(threadedTimeline)
+ override fun activeTimelineFlow(): StateFlow = flow
+ }
+ }
+ }
+ else -> Result.success(defaultTimelineProvider)
+ }
+ }
}
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 0f8cc5dc8f..48a222e0e7 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
@@ -25,6 +25,7 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.timeline.Timeline
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
@@ -52,7 +53,12 @@ class PollHistoryFlowNode @AssistedInject constructor(
return when (navTarget) {
is NavTarget.EditPoll -> {
createPollEntryPoint.nodeBuilder(this, buildContext)
- .params(CreatePollEntryPoint.Params(mode = CreatePollMode.EditPoll(eventId = navTarget.pollStartEventId)))
+ .params(
+ CreatePollEntryPoint.Params(
+ timelineMode = Timeline.Mode.Live,
+ mode = CreatePollMode.EditPoll(eventId = navTarget.pollStartEventId)
+ )
+ )
.build()
}
NavTarget.Root -> {
diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt
index e3d7632265..b144f31609 100644
--- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt
+++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt
@@ -67,10 +67,14 @@ class PollHistoryPresenter @Inject constructor(
coroutineScope.loadMore(timeline)
}
is PollHistoryEvents.SelectPollAnswer -> sessionCoroutineScope.launch {
- sendPollResponseAction.execute(pollStartId = event.pollStartId, answerId = event.answerId)
+ sendPollResponseAction.execute(
+ timeline = timeline,
+ pollStartId = event.pollStartId,
+ answerId = event.answerId
+ )
}
is PollHistoryEvents.EndPoll -> sessionCoroutineScope.launch {
- endPollAction.execute(pollStartId = event.pollStartId)
+ endPollAction.execute(timeline = timeline, pollStartId = event.pollStartId)
}
is PollHistoryEvents.SelectFilter -> {
activeFilter = event.filter
diff --git a/features/poll/impl/src/main/res/values-zh-rTW/translations.xml b/features/poll/impl/src/main/res/values-zh-rTW/translations.xml
index dd5b8eaf0f..5aa1693ef7 100644
--- a/features/poll/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/poll/impl/src/main/res/values-zh-rTW/translations.xml
@@ -5,6 +5,7 @@
"隱藏票數"
"選項 %1$d"
"變更尚未儲存,您確定要返回嗎?"
+ "刪除選項 %1$s"
"問題或主題"
"投什麼?"
"建立投票"
diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt
index 67556886bc..6bec98f1b9 100644
--- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt
+++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt
@@ -21,6 +21,7 @@ import io.element.android.features.poll.impl.anOngoingPollContent
import io.element.android.features.poll.impl.data.PollRepository
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.poll.PollKind
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
@@ -551,12 +552,18 @@ class CreatePollPresenterTest {
private fun createCreatePollPresenter(
mode: CreatePollMode = CreatePollMode.NewPoll,
room: FakeJoinedRoom = fakeJoinedRoom,
+ timelineMode: Timeline.Mode = Timeline.Mode.Live,
): CreatePollPresenter = CreatePollPresenter(
- repository = PollRepository(room, LiveTimelineProvider(room)),
+ repositoryFactory = object : PollRepository.Factory {
+ override fun create(timelineMode: Timeline.Mode): PollRepository {
+ return PollRepository(room, LiveTimelineProvider(room), timelineMode)
+ }
+ },
analyticsService = fakeAnalyticsService,
messageComposerContext = fakeMessageComposerContext,
navigateUp = { navUpInvocationsCount++ },
mode = mode,
+ timelineMode = timelineMode,
)
}
diff --git a/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/actions/FakeEndPollAction.kt b/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/actions/FakeEndPollAction.kt
index 6da99ad3d8..872110d0b6 100644
--- a/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/actions/FakeEndPollAction.kt
+++ b/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/actions/FakeEndPollAction.kt
@@ -9,6 +9,7 @@ package io.element.android.features.poll.test.actions
import io.element.android.features.poll.api.actions.EndPollAction
import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.timeline.Timeline
class FakeEndPollAction : EndPollAction {
private var executionCount = 0
@@ -17,7 +18,7 @@ class FakeEndPollAction : EndPollAction {
assert(executionCount == count)
}
- override suspend fun execute(pollStartId: EventId): Result {
+ override suspend fun execute(timeline: Timeline, pollStartId: EventId): Result {
executionCount++
return Result.success(Unit)
}
diff --git a/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/actions/FakeSendPollResponseAction.kt b/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/actions/FakeSendPollResponseAction.kt
index 859083a05c..f77d974178 100644
--- a/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/actions/FakeSendPollResponseAction.kt
+++ b/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/actions/FakeSendPollResponseAction.kt
@@ -9,6 +9,7 @@ package io.element.android.features.poll.test.actions
import io.element.android.features.poll.api.actions.SendPollResponseAction
import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.timeline.Timeline
class FakeSendPollResponseAction : SendPollResponseAction {
private var executionCount = 0
@@ -17,7 +18,7 @@ class FakeSendPollResponseAction : SendPollResponseAction {
assert(executionCount == count)
}
- override suspend fun execute(pollStartId: EventId, answerId: String): Result {
+ override suspend fun execute(timeline: Timeline, pollStartId: EventId, answerId: String): Result {
executionCount++
return Result.success(Unit)
}
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 c2edfc3d44..4a29222be7 100644
--- a/features/preferences/impl/src/main/res/values-da/translations.xml
+++ b/features/preferences/impl/src/main/res/values-da/translations.xml
@@ -13,6 +13,13 @@
"Upload fotos og videoer hurtigere, og reducér dataforbrug"
"Optimér mediekvaliteten"
"Moderation og sikkerhed"
+ "Optimér automatisk billeder for hurtigere uploads og mindre filstørrelser."
+ "Optimér kvaliteten på overførte billeder"
+ "%1$s. Tryk her for at ændre."
+ "Høj (1080p)"
+ "Lav (480p)"
+ "Standard (720p)"
+ "Kvalitet på overførte videoer"
"Udbyder af push-notifikationer"
"Deaktiver rich text-editoren for at skrive Markdown manuelt."
"Kvitteringer•for•læsning"
diff --git a/features/preferences/impl/src/main/res/values-en-rUS/translations.xml b/features/preferences/impl/src/main/res/values-en-rUS/translations.xml
index 796472c124..b1f615e697 100644
--- a/features/preferences/impl/src/main/res/values-en-rUS/translations.xml
+++ b/features/preferences/impl/src/main/res/values-en-rUS/translations.xml
@@ -1,4 +1,6 @@
"Optimize media quality"
+ "Automatically optimize images for faster uploads and smaller file sizes."
+ "Optimize image upload quality"
diff --git a/features/preferences/impl/src/main/res/values-et/translations.xml b/features/preferences/impl/src/main/res/values-et/translations.xml
index e9ee78f429..9b2f04a9f7 100644
--- a/features/preferences/impl/src/main/res/values-et/translations.xml
+++ b/features/preferences/impl/src/main/res/values-et/translations.xml
@@ -13,6 +13,13 @@
"Sellega laadid fotosid ja videoid kiiremini üles ning vähendad andmemahtu"
"Optimeeri meedia kvaliteeti"
"Modereerimine ja ohutus"
+ "Kiirema üleslaadimise ja väiksemate failide nimel optimeeri pilte automaatselt."
+ "Optimeeri üleslaaditavate piltide kvaliteeti."
+ "%1$s. Muutmiseks klõpsi siin."
+ "Kõrge (1080p)"
+ "Madal (480p)"
+ "Standard (720p)"
+ "Üleslaaditavate videote kvaliteet"
"Tõuketeavituste pakkuja"
"Kui soovid Markdown-vormingut käsitsi lisada, siis lülita vormindatud teksti toimeti välja."
"Lugemisteatised"
diff --git a/features/preferences/impl/src/main/res/values-eu/translations.xml b/features/preferences/impl/src/main/res/values-eu/translations.xml
index b624cd87f0..42a90e0328 100644
--- a/features/preferences/impl/src/main/res/values-eu/translations.xml
+++ b/features/preferences/impl/src/main/res/values-eu/translations.xml
@@ -6,12 +6,20 @@
"Garatzaile modua"
"Gaitu garatzaileentzako ezaugarrietarako eta funtzionalitateetarako sarbidea izateko."
"Igo argazkiak eta bideoak azkarrago eta murriztu datuen erabilera"
+ "Optimizatu multimediaren kalitatea"
"Moderazioa eta Segurtasuna"
+ "Optimizatu irudien igoera-kalitatea"
+ "Handia (1080p)"
+ "Txikia (480p)"
+ "Ertaina (720p)"
+ "Bideoen igoera-kalitatea"
"Push jakinarazpen hornitzailea"
"Desgaitu testu aberatseko editorea Markdown eskuz idazteko."
"Irakurketa-agiriak"
"Desaktibatutaz gero, ez zaizkio inori bidaliko mezuak irakurri izanaren agiriak. Beste erabiltzaile batzuen irakurketa-agiriak jasoko dituzu oraindik ere."
"Partekatu presentzia"
+ "Ezkutatu beti"
+ "Erakutsi beti"
"Gaitu aukera mezuaren iturria denbora-lerroan ikusteko."
"Ez duzu erabiltzailerik blokeatu"
"Desblokeatu"
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 1180454d03..dbb0b13326 100644
--- a/features/preferences/impl/src/main/res/values-fr/translations.xml
+++ b/features/preferences/impl/src/main/res/values-fr/translations.xml
@@ -13,6 +13,13 @@
"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é"
+ "Optimiser automatiquement les images pour des envois plus rapides et des tailles de fichiers plus petites."
+ "Optimiser la qualité des images envoyées"
+ "%1$s. Appuyez ici pour changer."
+ "Haute définition (1080p)"
+ "Basse résolution (480p)"
+ "Résolution standard (720p)"
+ "Qualité des vidéos envoyées"
"Fournisseur de Push"
"Désactivez l’éditeur de texte enrichi pour saisir manuellement du Markdown."
"Accusés de lecture"
diff --git a/features/preferences/impl/src/main/res/values-sv/translations.xml b/features/preferences/impl/src/main/res/values-sv/translations.xml
index 831280255c..e25e36a44f 100644
--- a/features/preferences/impl/src/main/res/values-sv/translations.xml
+++ b/features/preferences/impl/src/main/res/values-sv/translations.xml
@@ -13,6 +13,13 @@
"Ladda upp foton och videor snabbare och minska dataanvändningen"
"Optimera mediekvaliteten"
"Moderering och säkerhet"
+ "Optimera bilder automatiskt för snabbare uppladdningar och mindre filstorlekar."
+ "Optimera bilduppladdningskvalitet"
+ "%1$s. Tryck här för att ändra."
+ "Hög (1080p)"
+ "Låg (480p)"
+ "Standard (720p)"
+ "Videouppladdningskvalitet"
"Pushnotisleverantör"
"Inaktivera rik-text-redigeraren för att skriva Markdown manuellt."
"Läskvitton"
diff --git a/features/preferences/impl/src/main/res/values-uk/translations.xml b/features/preferences/impl/src/main/res/values-uk/translations.xml
index 22ca6bbc22..ba246e61f3 100644
--- a/features/preferences/impl/src/main/res/values-uk/translations.xml
+++ b/features/preferences/impl/src/main/res/values-uk/translations.xml
@@ -13,6 +13,10 @@
"Швидше завантажуйте фотографії та відео та зменшуйте використання даних"
"Оптимізуйте медіаякість"
"Модерування й безпека"
+ "Висока (1080p)"
+ "Низька (480p)"
+ "Стандартна (720p)"
+ "Якість вивантаження відео"
"Постачальник push-сповіщень"
"Вимкніть редактор розширеного тексту, щоб вводити Markdown вручну."
"Читати журнали"
diff --git a/features/preferences/impl/src/main/res/values-uz/translations.xml b/features/preferences/impl/src/main/res/values-uz/translations.xml
index e626620468..d8b27d0b5c 100644
--- a/features/preferences/impl/src/main/res/values-uz/translations.xml
+++ b/features/preferences/impl/src/main/res/values-uz/translations.xml
@@ -5,6 +5,7 @@
"Ishlab chiquvchilar uchun xususiyatlar va funksiyalarga kirishni yoqing."
"Maxsus element qo‘ng‘iroqlar bazasi URL manzili"
"Element qo\'ng\'irog\'iga maxsus asosiy url or\'natish"
+ "URL noto‘g‘ri, iltimos, protokol (http/https) va to‘g‘ri manzilni kiritganingizga ishonch hosil qiling."
"Boy matn muharriri o\'chiring Markdown bilan qo\'lda yozish uchun"
"Blokdan chiqarish"
"Ulardan kelgan barcha xabarlarni yana koʻrishingiz mumkin boʻladi."
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 b332624f13..668202a193 100644
--- a/features/preferences/impl/src/main/res/values-zh/translations.xml
+++ b/features/preferences/impl/src/main/res/values-zh/translations.xml
@@ -8,6 +8,8 @@
"自定义 Element Call URL"
"为 Element 通话设置根 URL。"
"URL 无效,请确保包含协议(http/https)和正确的地址。"
+ "在房间邀请请求中隐藏头像"
+ "在时间轴中隐藏媒体预览"
"针对上传进行优化"
"媒体"
"通知推送提供者"
@@ -16,6 +18,11 @@
"关闭后已读回执将不会发送给他人,但仍能收到他人的已读回执。"
"分享在线状态"
"关闭后将无法发送或接收已读回执、输入通知"
+ "始终隐藏"
+ "始终显示"
+ "在私人房间"
+ "随时可以通过点击隐藏的媒体来显示它"
+ "在时间轴中显示媒体"
"启用在时间轴中查看消息源码的选项。"
"您没有屏蔽用户"
"解封"
@@ -55,6 +62,7 @@
"系统设置"
"系统通知已关闭"
"通知"
+ "推送历史记录"
"排查问题"
"排查通知问题"
diff --git a/features/rageshake/impl/build.gradle.kts b/features/rageshake/impl/build.gradle.kts
index addfe3d794..0a3b4577f0 100644
--- a/features/rageshake/impl/build.gradle.kts
+++ b/features/rageshake/impl/build.gradle.kts
@@ -33,6 +33,7 @@ dependencies {
implementation(projects.libraries.network)
implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem)
+ implementation(projects.libraries.preferences.api)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.matrix.api)
@@ -56,6 +57,7 @@ dependencies {
testImplementation(projects.libraries.sessionStorage.implMemory)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.features.rageshake.test)
+ testImplementation(projects.libraries.preferences.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.services.toolbox.test)
testImplementation(libs.network.mockwebserver)
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt
index 0b86bb4ef0..88f3b5ce0e 100644
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt
@@ -7,32 +7,26 @@
package io.element.android.features.rageshake.impl.crash
-import android.content.Context
-import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
-import androidx.datastore.preferences.preferencesDataStore
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
-private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_crash")
-
private val appHasCrashedKey = booleanPreferencesKey("appHasCrashed")
private val crashDataKey = stringPreferencesKey("crashData")
@ContributesBinding(AppScope::class)
class PreferencesCrashDataStore @Inject constructor(
- @ApplicationContext context: Context
+ preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : CrashDataStore {
- private val store = context.dataStore
+ private val store = preferenceDataStoreFactory.create("elementx_crash")
override fun setCrashData(crashData: String) {
// Must block
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt
index ca310cedad..e41583b61c 100644
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt
@@ -7,7 +7,6 @@
package io.element.android.features.rageshake.impl.crash
-import android.content.Context
import android.os.Build
import io.element.android.libraries.core.data.tryOrNull
import timber.log.Timber
@@ -15,9 +14,8 @@ import java.io.PrintWriter
import java.io.StringWriter
class VectorUncaughtExceptionHandler(
- context: Context
+ private val preferencesCrashDataStore: PreferencesCrashDataStore,
) : Thread.UncaughtExceptionHandler {
- private val crashDataStore = PreferencesCrashDataStore(context)
private var previousHandler: Thread.UncaughtExceptionHandler? = null
/**
@@ -65,7 +63,7 @@ class VectorUncaughtExceptionHandler(
append(sw.buffer.toString())
}
Timber.e("FATAL EXCEPTION $bugDescription")
- crashDataStore.setCrashData(bugDescription)
+ preferencesCrashDataStore.setCrashData(bugDescription)
// Show the classical system popup
previousHandler?.uncaughtException(thread, throwable)
}
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeBindings.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeBindings.kt
new file mode 100644
index 0000000000..dc603f7b8f
--- /dev/null
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeBindings.kt
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.rageshake.impl.di
+
+import com.squareup.anvil.annotations.ContributesTo
+import io.element.android.features.rageshake.impl.crash.PreferencesCrashDataStore
+import io.element.android.libraries.di.AppScope
+
+@ContributesTo(AppScope::class)
+interface RageshakeBindings {
+ fun preferencesCrashDataStore(): PreferencesCrashDataStore
+}
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt
index 9d7171b8a0..0642f20e12 100644
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt
@@ -7,31 +7,25 @@
package io.element.android.features.rageshake.impl.rageshake
-import android.content.Context
-import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
-import androidx.datastore.preferences.preferencesDataStore
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
-private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_rageshake")
-
private val enabledKey = booleanPreferencesKey("enabled")
private val sensitivityKey = floatPreferencesKey("sensitivity")
@ContributesBinding(AppScope::class)
class PreferencesRageshakeDataStore @Inject constructor(
- @ApplicationContext context: Context
+ preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : RageshakeDataStore {
- private val store = context.dataStore
+ private val store = preferenceDataStoreFactory.create("elementx_rageshake")
override fun isEnabled(): Flow {
return store.data.map { prefs ->
diff --git a/features/rageshake/impl/src/main/res/values-zh-rTW/translations.xml b/features/rageshake/impl/src/main/res/values-zh-rTW/translations.xml
index cc2c14d907..2105ab1697 100644
--- a/features/rageshake/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/rageshake/impl/src/main/res/values-zh-rTW/translations.xml
@@ -10,6 +10,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 59da17e82d..80125d638a 100644
--- a/features/rageshake/impl/src/main/res/values-zh/translations.xml
+++ b/features/rageshake/impl/src/main/res/values-zh/translations.xml
@@ -10,6 +10,7 @@
"描述太短,请提供详细情况。谢谢!"
"发送崩溃日志"
"允许日志"
+ "日志文件过大,无法包含在本报告中,请通过其他方式发送给我们。"
"发送屏幕截图"
"为确认一切正常运行,您的消息中将包含日志。如要发送不带日志的消息,请关闭此设置。"
"%1$s 上次使用时崩溃了。想和我们分享崩溃报告吗?"
diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt
index f9f1aa72e4..f19362f99c 100644
--- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt
+++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt
@@ -9,28 +9,28 @@ package io.element.android.features.rageshake.impl.crash
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.AN_EXCEPTION
+import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
-import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class VectorUncaughtExceptionHandlerTest {
@Test
fun `activate should change the default handler`() {
- val sut = VectorUncaughtExceptionHandler(RuntimeEnvironment.getApplication())
+ val sut = VectorUncaughtExceptionHandler(PreferencesCrashDataStore(FakePreferenceDataStoreFactory()))
sut.activate()
assertThat(Thread.getDefaultUncaughtExceptionHandler()).isInstanceOf(VectorUncaughtExceptionHandler::class.java)
}
@Test
fun `uncaught exception`() = runTest {
- val crashDataStore = PreferencesCrashDataStore(RuntimeEnvironment.getApplication())
+ val crashDataStore = PreferencesCrashDataStore(FakePreferenceDataStoreFactory())
assertThat(crashDataStore.appHasCrashed().first()).isFalse()
assertThat(crashDataStore.crashInfo().first()).isEmpty()
- val sut = VectorUncaughtExceptionHandler(RuntimeEnvironment.getApplication())
+ val sut = VectorUncaughtExceptionHandler(crashDataStore)
sut.uncaughtException(Thread(), AN_EXCEPTION)
assertThat(crashDataStore.appHasCrashed().first()).isTrue()
val crashInfo = crashDataStore.crashInfo().first()
diff --git a/features/reportroom/impl/src/main/res/values-zh/translations.xml b/features/reportroom/impl/src/main/res/values-zh/translations.xml
new file mode 100644
index 0000000000..40a0b5f1b0
--- /dev/null
+++ b/features/reportroom/impl/src/main/res/values-zh/translations.xml
@@ -0,0 +1,8 @@
+
+
+ "您的报告已成功提交,但在尝试离开房间时遇到了问题。请重试。"
+ "无法离开房间"
+ "向管理员举报此房间。如果信息已加密,管理员将无法读取。"
+ "描述举报的原因…"
+ "举报房间"
+
diff --git a/features/roomaliasresolver/impl/src/main/res/values-zh/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-zh/translations.xml
index 633d2131c2..52934b6a08 100644
--- a/features/roomaliasresolver/impl/src/main/res/values-zh/translations.xml
+++ b/features/roomaliasresolver/impl/src/main/res/values-zh/translations.xml
@@ -1,4 +1,5 @@
+ "无法显示此房间预览"
"无法解析聊天室别名。"
diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts
index ad45eda8fc..302ae7d8c6 100644
--- a/features/roomdetails/impl/build.gradle.kts
+++ b/features/roomdetails/impl/build.gradle.kts
@@ -44,7 +44,7 @@ dependencies {
api(projects.services.apperror.api)
implementation(libs.coil.compose)
implementation(projects.features.call.api)
- implementation(projects.features.createroom.api)
+ implementation(projects.features.startchat.api)
implementation(projects.features.leaveroom.api)
implementation(projects.features.userprofile.shared)
implementation(projects.services.analytics.compose)
@@ -56,6 +56,7 @@ dependencies {
implementation(projects.features.reportroom.api)
implementation(projects.features.roommembermoderation.api)
implementation(projects.features.changeroommemberroles.api)
+ implementation(projects.features.invitepeople.api)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
@@ -72,7 +73,7 @@ dependencies {
testImplementation(projects.libraries.usersearch.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.tests.testutils)
- testImplementation(projects.features.createroom.test)
+ testImplementation(projects.features.startchat.test)
testImplementation(projects.services.analytics.test)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersEvents.kt
deleted file mode 100644
index a06ae82c49..0000000000
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersEvents.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.roomdetails.impl.invite
-
-import io.element.android.libraries.matrix.api.user.MatrixUser
-
-sealed interface RoomInviteMembersEvents {
- data class ToggleUser(val user: MatrixUser) : RoomInviteMembersEvents
- data class UpdateSearchQuery(val query: String) : RoomInviteMembersEvents
- data class OnSearchActiveChanged(val active: Boolean) : RoomInviteMembersEvents
-}
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 ebfa3dcab8..dc269e7322 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
@@ -9,7 +9,6 @@ package io.element.android.features.roomdetails.impl.invite
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
@@ -18,28 +17,21 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.anvilannotations.ContributesNode
-import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.features.invitepeople.api.InvitePeoplePresenter
+import io.element.android.features.invitepeople.api.InvitePeopleRenderer
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.room.JoinedRoom
-import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
-import io.element.android.services.apperror.api.AppErrorStateService
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.launch
@ContributesNode(RoomScope::class)
class RoomInviteMembersNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- coroutineDispatchers: CoroutineDispatchers,
- private val room: JoinedRoom,
- private val presenter: RoomInviteMembersPresenter,
- private val appErrorStateService: AppErrorStateService,
private val analyticsService: AnalyticsService,
+ private val invitePeopleRenderer: InvitePeopleRenderer,
+ room: JoinedRoom,
+ invitePeoplePresenterFactory: InvitePeoplePresenter.Factory,
) : Node(buildContext, plugins = plugins) {
- private val coroutineScope = CoroutineScope(SupervisorJob() + coroutineDispatchers.io)
-
init {
lifecycle.subscribe(
onResume = {
@@ -48,31 +40,21 @@ class RoomInviteMembersNode @AssistedInject constructor(
)
}
+ private val invitePeoplePresenter = invitePeoplePresenterFactory.create(
+ joinedRoom = room,
+ roomId = room.roomId,
+ )
+
@Composable
override fun View(modifier: Modifier) {
- val state = presenter.present()
- val context = LocalContext.current.applicationContext
-
+ val state = invitePeoplePresenter.present()
RoomInviteMembersView(
state = state,
modifier = modifier,
onBackClick = { navigateUp() },
- onSubmitClick = { users ->
- navigateUp()
-
- coroutineScope.launch {
- val anyInviteFailed = users
- .map { room.inviteUserById(it.userId) }
- .any { it.isFailure }
-
- if (anyInviteFailed) {
- appErrorStateService.showError(
- title = context.getString(CommonStrings.common_unable_to_invite_title),
- body = context.getString(CommonStrings.common_unable_to_invite_message),
- )
- }
- }
- }
- )
+ onDone = { navigateUp() }
+ ) {
+ invitePeopleRenderer.Render(state, Modifier)
+ }
}
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt
index caae6ff16d..8bec90707f 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt
@@ -7,49 +7,34 @@
package io.element.android.features.roomdetails.impl.invite
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Box
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.itemsIndexed
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.unit.dp
-import io.element.android.compound.theme.ElementTheme
+import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.features.invitepeople.api.InvitePeopleState
+import io.element.android.features.invitepeople.api.InvitePeopleStateProvider
import io.element.android.features.roomdetails.impl.R
-import io.element.android.libraries.designsystem.components.async.AsyncLoading
-import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Scaffold
-import io.element.android.libraries.designsystem.theme.components.SearchBar
-import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
-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.user.MatrixUser
-import io.element.android.libraries.matrix.ui.components.CheckableUserRow
-import io.element.android.libraries.matrix.ui.components.CheckableUserRowData
-import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
-import io.element.android.libraries.matrix.ui.model.getAvatarData
-import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.ui.strings.CommonStrings
-import kotlinx.collections.immutable.ImmutableList
@Composable
fun RoomInviteMembersView(
- state: RoomInviteMembersState,
+ state: InvitePeopleState,
onBackClick: () -> Unit,
- onSubmitClick: (List) -> Unit,
+ onDone: () -> Unit,
modifier: Modifier = Modifier,
+ invitePeopleView: @Composable () -> Unit,
) {
Scaffold(
modifier = modifier,
@@ -57,44 +42,26 @@ fun RoomInviteMembersView(
RoomInviteMembersTopBar(
onBackClick = {
if (state.isSearchActive) {
- state.eventSink(RoomInviteMembersEvents.OnSearchActiveChanged(false))
+ state.eventSink(InvitePeopleEvents.CloseSearch)
} else {
onBackClick()
}
},
- onSubmitClick = { onSubmitClick(state.selectedUsers) },
+ onSubmitClick = {
+ state.eventSink(InvitePeopleEvents.SendInvites)
+ onDone()
+ },
canSend = state.canInvite,
)
}
) { padding ->
- Column(
+ Box(
modifier = Modifier
.fillMaxWidth()
.padding(padding)
.consumeWindowInsets(padding),
- verticalArrangement = Arrangement.spacedBy(16.dp),
) {
- RoomInviteMembersSearchBar(
- modifier = Modifier.fillMaxWidth(),
- query = state.searchQuery,
- showLoader = state.showSearchLoader,
- selectedUsers = state.selectedUsers,
- state = state.searchResults,
- active = state.isSearchActive,
- onActiveChange = { state.eventSink(RoomInviteMembersEvents.OnSearchActiveChanged(it)) },
- onTextChange = { state.eventSink(RoomInviteMembersEvents.UpdateSearchQuery(it)) },
- onToggleUser = { state.eventSink(RoomInviteMembersEvents.ToggleUser(it)) },
- )
-
- if (!state.isSearchActive) {
- SelectedUsersRowList(
- modifier = Modifier.fillMaxWidth(),
- selectedUsers = state.selectedUsers,
- autoScroll = true,
- onUserRemove = { state.eventSink(RoomInviteMembersEvents.ToggleUser(it)) },
- contentPadding = PaddingValues(16.dp),
- )
- }
+ invitePeopleView()
}
}
}
@@ -119,101 +86,13 @@ private fun RoomInviteMembersTopBar(
)
}
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun RoomInviteMembersSearchBar(
- query: String,
- state: SearchBarResultState>,
- showLoader: Boolean,
- selectedUsers: ImmutableList,
- active: Boolean,
- onActiveChange: (Boolean) -> Unit,
- onTextChange: (String) -> Unit,
- onToggleUser: (MatrixUser) -> Unit,
- modifier: Modifier = Modifier,
- placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone),
-) {
- SearchBar(
- query = query,
- onQueryChange = onTextChange,
- active = active,
- onActiveChange = onActiveChange,
- modifier = modifier,
- placeHolderTitle = placeHolderTitle,
- contentPrefix = {
- if (selectedUsers.isNotEmpty()) {
- SelectedUsersRowList(
- modifier = Modifier.fillMaxWidth(),
- selectedUsers = selectedUsers,
- autoScroll = true,
- onUserRemove = onToggleUser,
- contentPadding = PaddingValues(16.dp),
- )
- }
- },
- showBackButton = false,
- resultState = state,
- contentSuffix = {
- if (showLoader) {
- AsyncLoading()
- }
- },
- resultHandler = { results ->
- Text(
- text = stringResource(id = CommonStrings.common_search_results),
- style = ElementTheme.typography.fontBodyLgMedium,
- modifier = Modifier
- .fillMaxWidth()
- .padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 8.dp)
- )
-
- LazyColumn {
- itemsIndexed(results) { index, invitableUser ->
- val notInvitedOrJoined = !(invitableUser.isAlreadyInvited || invitableUser.isAlreadyJoined)
- val isUnresolved = invitableUser.isUnresolved && notInvitedOrJoined
- val enabled = isUnresolved || notInvitedOrJoined
- val data = if (isUnresolved) {
- CheckableUserRowData.Unresolved(
- avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem),
- id = invitableUser.matrixUser.userId.value,
- )
- } else {
- CheckableUserRowData.Resolved(
- avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem),
- name = invitableUser.matrixUser.getBestName(),
- subtext = when {
- // If they're already invited or joined we show that information
- invitableUser.isAlreadyJoined -> stringResource(R.string.screen_room_details_already_a_member)
- invitableUser.isAlreadyInvited -> stringResource(R.string.screen_room_details_already_invited)
- // Otherwise show the ID, unless that's already used for their name
- invitableUser.matrixUser.displayName.isNullOrEmpty().not() -> invitableUser.matrixUser.userId.value
- else -> null
- }
- )
- }
- CheckableUserRow(
- checked = invitableUser.isSelected,
- enabled = enabled,
- data = data,
- onCheckedChange = { onToggleUser(invitableUser.matrixUser) },
- modifier = Modifier.fillMaxWidth()
- )
-
- if (index < results.lastIndex) {
- HorizontalDivider()
- }
- }
- }
- },
- )
-}
-
@PreviewsDayNight
@Composable
-internal fun RoomInviteMembersViewPreview(@PreviewParameter(RoomInviteMembersStateProvider::class) state: RoomInviteMembersState) = ElementPreview {
+internal fun RoomInviteMembersViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview {
RoomInviteMembersView(
state = state,
+ invitePeopleView = {},
onBackClick = {},
- onSubmitClick = {},
+ onDone = {},
)
}
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 d50b3c5f6e..01c076fdb5 100644
--- a/features/roomdetails/impl/src/main/res/values-be/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-be/translations.xml
@@ -33,8 +33,6 @@
"У вас ёсць незахаваныя змены."
"Захаваць змены?"
"Дадаць тэму"
- "Ужо ўдзельнік"
- "Ужо запрасілі"
"Зашыфраваны"
"Не зашыфраваны"
"Публічны пакой"
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 6f9c256742..fb01acb4fb 100644
--- a/features/roomdetails/impl/src/main/res/values-bg/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-bg/translations.xml
@@ -22,8 +22,6 @@
"Модератори"
"Членове"
"Добавяне на тема"
- "Вече е член"
- "Вече е бил поканен"
"С шифроване"
"Без шифроване"
"Общодостъпна стая"
diff --git a/features/roomdetails/impl/src/main/res/values-cs/translations.xml b/features/roomdetails/impl/src/main/res/values-cs/translations.xml
index 3bd037b1c0..d484486940 100644
--- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml
@@ -35,8 +35,6 @@
"Máte neuložené změny."
"Uložit změny?"
"Přidat téma"
- "Již členem"
- "Již pozván(a)"
"Šifrováno"
"Není šifrováno"
"Veřejná místnost"
diff --git a/features/roomdetails/impl/src/main/res/values-cy/translations.xml b/features/roomdetails/impl/src/main/res/values-cy/translations.xml
index ab0a8fa95d..80726aa1f8 100644
--- a/features/roomdetails/impl/src/main/res/values-cy/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-cy/translations.xml
@@ -35,8 +35,6 @@
"Mae gennych newidiadau heb eu cadw."
"Cadw\'r newidiadau?"
"Ychwanegu pwnc"
- "Eisoes yn aelod"
- "Wedi gwahodd yn barod"
"Wedi\'i amgryptio"
"Heb ei amgryptio"
"Ystafell gyhoeddus"
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 e989977145..a07d86be77 100644
--- a/features/roomdetails/impl/src/main/res/values-da/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-da/translations.xml
@@ -39,8 +39,6 @@
"Du har ændringer, der ikke er gemt."
"Gem ændringer?"
"Tilføj emne"
- "Allerede medlem"
- "Allerede inviteret"
"Krypteret"
"Ikke krypteret"
"Offentligt rum"
diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml
index 2a2673beb1..714d36c38a 100644
--- a/features/roomdetails/impl/src/main/res/values-de/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml
@@ -35,8 +35,6 @@
"Sie haben ungespeicherte Änderungen."
"Änderungen speichern?"
"Thema hinzufügen"
- "Bereits Mitglied"
- "Bereits eingeladen"
"Verschlüsselt"
"Nicht verschlüsselt"
"Öffentlicher Raum"
diff --git a/features/roomdetails/impl/src/main/res/values-el/translations.xml b/features/roomdetails/impl/src/main/res/values-el/translations.xml
index aed10ce24f..678dad15c9 100644
--- a/features/roomdetails/impl/src/main/res/values-el/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-el/translations.xml
@@ -35,8 +35,6 @@
"Έχεις μη αποθηκευμένες αλλαγές."
"Αποθήκευση αλλαγών;"
"Προσθήκη θέματος"
- "Ήδη μέλος"
- "Ήδη προσκεκλημένος"
"Κρυπτογραφημένο"
"Μη κρυπτογραφημένο"
"Δημόσια αίθουσα"
diff --git a/features/roomdetails/impl/src/main/res/values-es/translations.xml b/features/roomdetails/impl/src/main/res/values-es/translations.xml
index ffce0a6b76..96e31eb5c5 100644
--- a/features/roomdetails/impl/src/main/res/values-es/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-es/translations.xml
@@ -35,8 +35,6 @@
"Tienes cambios sin guardar."
"¿Guardar cambios?"
"Añadir tema"
- "Ya eres miembro"
- "Ya estás invitado"
"Cifrada"
"No cifrada"
"Sala pública"
diff --git a/features/roomdetails/impl/src/main/res/values-et/translations.xml b/features/roomdetails/impl/src/main/res/values-et/translations.xml
index efc6ee8e6d..4426ca1e77 100644
--- a/features/roomdetails/impl/src/main/res/values-et/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-et/translations.xml
@@ -7,7 +7,7 @@
"Küsitlused"
"Vaid peakasutajad"
"Suhtluskeelu seadmine"
- "Sõnumite kustutamine"
+ "Eemalda sõnumid"
"Kõik"
"Kutsu teisi osalejaid ja vasta ise liitumiskutsetele"
"Jututoas osalejate modereerimine"
@@ -39,8 +39,6 @@
"Sul on salvestamata muudatusi"
"Kas salvestame muudatused?"
"Lisa teema"
- "Sa juba oled jututoa liige"
- "Sa juba oled kutse saanud"
"Krüptitud jututuba"
"Krüptimata jututuba"
"Avalik jututuba"
diff --git a/features/roomdetails/impl/src/main/res/values-eu/translations.xml b/features/roomdetails/impl/src/main/res/values-eu/translations.xml
index bd2a370942..64ec4ad928 100644
--- a/features/roomdetails/impl/src/main/res/values-eu/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-eu/translations.xml
@@ -20,6 +20,7 @@
"Bidali mezuak"
"Editatu administratzaileak"
"Administratzailea gehitu?"
+ "Jabetza eskualdatu?"
"Jaitsi mailaz"
"Ezin izango duzu hau aldatu zure burua mailaz jaisten ari zarelako, zu bazara gelan baimenak dituen azken erabiltzailea ezin izango dira baimenak berreskuratu."
"Zure burua mailaz jaitsi?"
@@ -27,14 +28,13 @@
"(Egiteke)"
"Administratzaileek automatikoki dute moderatzaile-pribilegioak"
"Editatu moderatzaileak"
+ "Aukeratu jabeak"
"Administratzaileak"
"Moderatzaileak"
"Kideak"
"Gorde gabeko aldaketak dituzu."
"Aldaketak gorde?"
"Gehitu hizketagaia"
- "Kidea da dagoeneko"
- "Lehendik ere gonbidatuta"
"Zifratuta"
"Zifratu gabe"
"Gela publikoa"
@@ -76,6 +76,7 @@
"Zain"
"Kudeatzailea"
"Moderatzailea"
+ "Jabea"
"Gelako kideak"
"%1$s(r)i debekua kentzen"
"Aktibatuz gero, defektuzko ezarpena gainidatziko du"
@@ -89,12 +90,14 @@
"Aipamenak eta hitz gakoak soilik"
"Gela honetan, jakinarazi"
"Administratzaileak"
+ "Administratzaileak eta jabeak"
"Aldatu nire rola"
"Jaitsi maila, kidera"
"Jaitsi maila, moderatzailera"
"Kideen moderazioa"
"Mezuak eta edukiak"
"Moderatzaileak"
+ "Jabeak"
"Baimenak"
"Berrezarri baimenak"
"Baimenak berrezarritakoan, uneko ezarpenak galduko dituzu."
diff --git a/features/roomdetails/impl/src/main/res/values-fa/translations.xml b/features/roomdetails/impl/src/main/res/values-fa/translations.xml
index 7941f60609..2de731b612 100644
--- a/features/roomdetails/impl/src/main/res/values-fa/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-fa/translations.xml
@@ -34,8 +34,6 @@
"تغییراتی ذخیره نشده دارید."
"ذخیرهٔ تغییرات؟"
"افزودن موضوع"
- "از پیش عضو است"
- "از پیش دعوت شده"
"رمز شده"
"رمزنگاری نشده"
"اتاق عمومی"
@@ -65,6 +63,10 @@
"اطّلاعات اتاق"
"موضوع"
"بهروز کردن اتاق…"
+
+ - "%1$d نفر"
+ - "%1$d نفر"
+
"برداشت و تحریم عضو"
"تنها برداشتن عضو"
"رفع انسداد"
diff --git a/features/roomdetails/impl/src/main/res/values-fi/translations.xml b/features/roomdetails/impl/src/main/res/values-fi/translations.xml
index 5906bd5986..04c1411e50 100644
--- a/features/roomdetails/impl/src/main/res/values-fi/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-fi/translations.xml
@@ -39,8 +39,6 @@
"Sinulla on tallentamattomia muutoksia"
"Tallenna muutokset?"
"Lisää aihe"
- "On jo jäsen"
- "On jo kutsuttu"
"Salattu"
"Ei salattu"
"Julkinen huone"
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 f5b876c9f7..066ce1ee0f 100644
--- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml
@@ -39,8 +39,6 @@
"Vous avez des modifications non-enregistrées."
"Enregistrer les changements ?"
"Ajouter un sujet"
- "Déjà membre"
- "Déjà invité(e)"
"Chiffré"
"Non chiffré"
"Salon public"
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 dfe25cf712..6e2484fc68 100644
--- a/features/roomdetails/impl/src/main/res/values-hu/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-hu/translations.xml
@@ -39,8 +39,6 @@
"Mentetlen módosításai vannak."
"Menti a módosításokat?"
"Téma hozzáadása"
- "Már tag"
- "Már meghívták"
"Titkosított"
"Nem titkosított"
"Nyilvános szoba"
diff --git a/features/roomdetails/impl/src/main/res/values-in/translations.xml b/features/roomdetails/impl/src/main/res/values-in/translations.xml
index 2143418fb0..c78485bc4d 100644
--- a/features/roomdetails/impl/src/main/res/values-in/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-in/translations.xml
@@ -7,7 +7,7 @@
"Pemungutan suara"
"Hanya admin"
"Cekal orang-orang"
- "Hapus pesan"
+ "Hilangkan pesan"
"Semua orang"
"Undang orang-orang dan terima permintaan untuk bergabung"
"Moderasi anggota"
@@ -35,8 +35,6 @@
"Anda memiliki perubahan yang belum disimpan."
"Simpan perubahan?"
"Tambahkan topik"
- "Sudah menjadi anggota"
- "Sudah diundang"
"Terenkripsi"
"Tidak terenkripsi"
"Ruangan publik"
diff --git a/features/roomdetails/impl/src/main/res/values-it/translations.xml b/features/roomdetails/impl/src/main/res/values-it/translations.xml
index 54de5f5407..988a2edafd 100644
--- a/features/roomdetails/impl/src/main/res/values-it/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml
@@ -35,8 +35,6 @@
"Hai delle modifiche non salvate."
"Salvare le modifiche?"
"Aggiungi argomento"
- "Già membro"
- "Già invitato"
"Cifrata"
"Non cifrata"
"Stanza pubblica"
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 6a68985d87..1beb70d4dd 100644
--- a/features/roomdetails/impl/src/main/res/values-ka/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-ka/translations.xml
@@ -32,8 +32,6 @@
"თქვენ გაქვთ შეუნახავი ცვლილებები"
"შენახვა?"
"თემის დამატება"
- "უკვე წევრია"
- "უკვე მოწვეულია"
"ოთახის რედაქტირება"
"უცნობი შეცდომა მოხდა. ინფორმაციის შეცვლა ვერ მოხერხდა."
"ოთახის განახლება შეუძლებელია"
diff --git a/features/roomdetails/impl/src/main/res/values-lt/translations.xml b/features/roomdetails/impl/src/main/res/values-lt/translations.xml
index 18a908b2c0..1b3b44fdf2 100644
--- a/features/roomdetails/impl/src/main/res/values-lt/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-lt/translations.xml
@@ -1,8 +1,6 @@
"Pridėti temą"
- "Jau narys"
- "Jau pakviestas"
"Redaguoti kambarį"
"Įvyko nežinoma klaida ir informacijos pakeisti nepavyko."
"Nepavyko atnaujinti kambario"
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 a8f5d8d123..f964f142b5 100644
--- a/features/roomdetails/impl/src/main/res/values-nb/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-nb/translations.xml
@@ -35,8 +35,6 @@
"Du har endringer som ikke er lagret."
"Lagre endringer?"
"Legg til emne"
- "Allerede medlem"
- "Allerede invitert"
"Kryptert"
"Ikke kryptert"
"Offentlig rom"
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 2b190fc4db..9b932f60fe 100644
--- a/features/roomdetails/impl/src/main/res/values-nl/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-nl/translations.xml
@@ -33,8 +33,6 @@
"Je hebt niet-opgeslagen wijzigingen"
"Wijzigingen opslaan?"
"Onderwerp toevoegen"
- "Reeds lid"
- "Reeds uitgenodigd"
"Versleuteld"
"Niet versleuteld"
"Openbare kamer"
diff --git a/features/roomdetails/impl/src/main/res/values-pl/translations.xml b/features/roomdetails/impl/src/main/res/values-pl/translations.xml
index c708ac220a..b9847c6cfd 100644
--- a/features/roomdetails/impl/src/main/res/values-pl/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-pl/translations.xml
@@ -39,8 +39,6 @@
"Masz niezapisane zmiany."
"Zapisać zmiany?"
"Dodaj temat"
- "Jest już członkiem"
- "Już zaproszony"
"Szyfrowany"
"Nieszyfrowany"
"Pokój publiczny"
diff --git a/features/roomdetails/impl/src/main/res/values-pt-rBR/translations.xml b/features/roomdetails/impl/src/main/res/values-pt-rBR/translations.xml
index 5561005e9b..bfe0b44af2 100644
--- a/features/roomdetails/impl/src/main/res/values-pt-rBR/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-pt-rBR/translations.xml
@@ -35,8 +35,6 @@
"Você tem alterações não salvas."
"Salvar alterações?"
"Adicionar tópico"
- "Já é membro"
- "Já foi convidado"
"Criptografado"
"Não criptografado"
"Sala pública"
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 32d1bb8fc8..cba9ab5ac1 100644
--- a/features/roomdetails/impl/src/main/res/values-pt/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-pt/translations.xml
@@ -39,8 +39,6 @@
"Tens alterações por guardar."
"Guardar alterações?"
"Adicionar descrição"
- "Já é participante"
- "Já foi convidado"
"Cifrada"
"Não cifrada"
"Sala pública"
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 622278dc64..b901be078e 100644
--- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml
@@ -33,8 +33,6 @@
"Aveți modificări nesalvate."
"Salvați modificările?"
"Adăugare subiect"
- "Deja membru"
- "Deja invitat"
"Criptat"
"Necriptat"
"Cameră publică"
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 77d0b94eb3..e4c53ff30d 100644
--- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml
@@ -35,8 +35,6 @@
"У вас есть несохраненные изменения."
"Сохранить изменения?"
"Добавить тему"
- "Уже зарегистрирован"
- "Уже приглашены"
"Зашифровано"
"Шифрования нет"
"Общедоступная комната"
diff --git a/features/roomdetails/impl/src/main/res/values-sk/translations.xml b/features/roomdetails/impl/src/main/res/values-sk/translations.xml
index b8e9e0c30a..530455cbf2 100644
--- a/features/roomdetails/impl/src/main/res/values-sk/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml
@@ -39,8 +39,6 @@
"Máte neuložené zmeny."
"Uložiť zmeny?"
"Pridať tému"
- "Už ste členom"
- "Už ste pozvaní"
"Zašifrované"
"Nešifrované"
"Verejná miestnosť"
diff --git a/features/roomdetails/impl/src/main/res/values-sv/translations.xml b/features/roomdetails/impl/src/main/res/values-sv/translations.xml
index 12cec6d6ee..1cc11933ab 100644
--- a/features/roomdetails/impl/src/main/res/values-sv/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-sv/translations.xml
@@ -22,21 +22,23 @@
"Redigera administratörer"
"Du kommer inte att kunna ångra den här åtgärden. Du befordrar användaren till att ha samma behörighetsnivå som du."
"Lägg till Admin?"
+ "Du kommer inte att kunna ångra den här åtgärden. Du överför ägarskapet till de valda användarna. När du lämnar kommer detta att vara permanent."
+ "Överför ägarskap?"
"Degradera"
"Du kommer inte att kunna ångra denna ändring eftersom du degraderar dig själv, om du är den sista privilegierade användaren i rummet kommer det att vara omöjligt att återfå privilegier."
"Degradera dig själv?"
"%1$s (Väntar)"
"(Väntar)"
"Administratörer har automatiskt moderatorbehörighet"
+ "Ägare har automatiskt administratörsbehörighet."
"Redigera moderatorer"
+ "Välj ägare"
"Administratörer"
"Moderatorer"
"Medlemmar"
"Du har osparade ändringar."
"Spara ändringar?"
"Lägg till ämne"
- "Redan medlem"
- "Redan inbjuden"
"Krypterat"
"Inte krypterat"
"Offentligt rum"
@@ -81,6 +83,7 @@
"Väntar"
"Admin"
"Moderator"
+ "Ägare"
"Rumsmedlemmar"
"Avbannar %1$s"
"Tillåt anpassad inställning"
@@ -98,12 +101,14 @@
"Endast omnämnanden och nyckelord"
"I det här rummet, meddela mig för"
"Administratörer"
+ "Administratörer och ägare"
"Ändra min roll"
"Degradera till medlem"
"Degradera till moderator"
"Medlemsmoderering"
"Meddelanden och innehåll"
"Moderatorer"
+ "Ägare"
"Behörigheter"
"Återställ behörigheter"
"När du har återställt behörigheterna kommer du att förlora de aktuella inställningarna."
diff --git a/features/roomdetails/impl/src/main/res/values-tr/translations.xml b/features/roomdetails/impl/src/main/res/values-tr/translations.xml
index 67a8845821..92caaaf5eb 100644
--- a/features/roomdetails/impl/src/main/res/values-tr/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-tr/translations.xml
@@ -35,8 +35,6 @@
"Kaydedilmemiş değişiklikleriniz var."
"Değişiklikleri Kaydet?"
"Konu ekle"
- "Zaten üye"
- "Zaten davet edildi"
"Şifrelenmiş"
"Şifrelenmemiş"
"Herkese açık oda"
diff --git a/features/roomdetails/impl/src/main/res/values-uk/translations.xml b/features/roomdetails/impl/src/main/res/values-uk/translations.xml
index b3cac2e00a..7ef99d8430 100644
--- a/features/roomdetails/impl/src/main/res/values-uk/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-uk/translations.xml
@@ -39,8 +39,6 @@
"У вас є не збережені зміни."
"Зберегти зміни?"
"Додати тему"
- "Уже учасник"
- "Уже запрошені"
"Зашифровано"
"Не зашифровано"
"Загальнодоступна кімната"
diff --git a/features/roomdetails/impl/src/main/res/values-ur/translations.xml b/features/roomdetails/impl/src/main/res/values-ur/translations.xml
index 00c90107ac..12a8d964cb 100644
--- a/features/roomdetails/impl/src/main/res/values-ur/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-ur/translations.xml
@@ -33,8 +33,6 @@
"آپکے پاس غیر محفوظ تبدیلیاں ہیں"
"تبدیلیاں محفوظ کریں؟"
"موضوع شامل کریں"
- "پہلے سے ہی رکن"
- "پہلے سے مدعو شدہ"
"مرموز کردہ"
"رموز کردہ نہیں"
"عوامی کمرہ"
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 030e0a91bd..229b201926 100644
--- a/features/roomdetails/impl/src/main/res/values-uz/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-uz/translations.xml
@@ -3,8 +3,6 @@
"Bildirishnoma sozlamalarini yangilashda xatolik yuz berdi."
"Har kim"
"Mavzu qo\'shish"
- "Allaqachon a\'zo"
- "Allaqachon taklif qilingan"
"Xonani tahrirlash"
"Nomaʼlum xatolik yuz berdi va maʼlumotni oʻzgartirib boʻlmadi."
"Xonani yangilab bo‘lmadi"
@@ -14,6 +12,7 @@
"Bu xona ovozini o‘chirib bo‘lmadi, qayta urinib ko‘ring."
"Bu xonaning ovozi yoqilmadi, qayta urinib ko‘ring."
"Odamlarni taklif qiling"
+ "Suhbatni tark etish"
"Xonani tark etish"
"Maxsus"
"Standart"
@@ -42,4 +41,5 @@
"Barcha xabarlar"
"Faqat eslatmalar va kalit so\'zlar"
"Bu xonada menga xabar bering"
+ "Shifrlash"
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 ac32c3597b..d2d6d0ef90 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
@@ -22,21 +22,23 @@
"編輯管理員"
"您將無法復原此動作。您正將使用者提昇至與您相同的權力等級。"
"要新增管理員嗎?"
+ "您將無法撤銷此動作。您正在將所有權轉移給選定的使用者。一旦您離開,此動作將永久有效。"
+ "轉移所有權?"
"降級"
"當您自行降級時,您將無法復原此變更,若您是聊天室中的最後一位特權使用者,則無法重新獲得權限。"
"將自己降級?"
"%1$s(擱置中)"
"(擱置中)"
"管理員自動擁有版主權限"
+ "擁有者自動擁有管理員權限。"
"編輯版主"
+ "選擇擁有者"
"管理員"
"版主"
"成員"
"您有尚未儲存的變更"
"是否儲存變更?"
"新增主題"
- "已是成員"
- "已邀請"
"已加密"
"未加密"
"公開的聊天室"
@@ -80,6 +82,7 @@
"待定"
"管理員"
"版主"
+ "擁有者"
"聊天室成員"
"正在解除黑名單 %1$s"
"允許自訂設定"
@@ -97,12 +100,14 @@
"僅限提及與關鍵字"
"在此聊天適中,通知我"
"管理員"
+ "管理員與擁有者"
"變更我的身份"
"降級為普通成員"
"降級為版主"
"成員管理"
"訊息與內容"
"版主"
+ "擁有者"
"權限"
"重設權限"
"重設之後,您會遺失當前的設定。"
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 367741e4ef..7d5a9e330c 100644
--- a/features/roomdetails/impl/src/main/res/values-zh/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-zh/translations.xml
@@ -35,8 +35,6 @@
"您有未保存的更改。"
"保存更改?"
"添加主题"
- "已经是成员"
- "已邀请"
"加密的"
"未加密的"
"公共聊天室"
diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml
index d0c389ded5..5dd185ec13 100644
--- a/features/roomdetails/impl/src/main/res/values/localazy.xml
+++ b/features/roomdetails/impl/src/main/res/values/localazy.xml
@@ -39,8 +39,6 @@
"You have unsaved changes."
"Save changes?"
"Add topic"
- "Already a member"
- "Already invited"
"Encrypted"
"Not encrypted"
"Public room"
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt
deleted file mode 100644
index 5aae0f5d3b..0000000000
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt
+++ /dev/null
@@ -1,408 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.roomdetails.impl.invite
-
-import app.cash.molecule.RecompositionMode
-import app.cash.molecule.moleculeFlow
-import app.cash.turbine.test
-import com.google.common.truth.Truth.assertThat
-import io.element.android.features.roomdetails.impl.aRoom
-import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
-import io.element.android.features.roomdetails.impl.members.aRoomMember
-import io.element.android.features.roomdetails.impl.members.aRoomMemberList
-import io.element.android.libraries.core.coroutine.CoroutineDispatchers
-import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
-import io.element.android.libraries.matrix.api.room.BaseRoom
-import io.element.android.libraries.matrix.api.room.RoomMembersState
-import io.element.android.libraries.matrix.api.room.RoomMembershipState
-import io.element.android.libraries.matrix.api.user.MatrixUser
-import io.element.android.libraries.matrix.test.A_USER_ID
-import io.element.android.libraries.matrix.test.A_USER_ID_2
-import io.element.android.libraries.matrix.test.room.FakeBaseRoom
-import io.element.android.libraries.matrix.ui.components.aMatrixUser
-import io.element.android.libraries.matrix.ui.components.aMatrixUserList
-import io.element.android.libraries.usersearch.api.UserSearchResult
-import io.element.android.libraries.usersearch.api.UserSearchResultState
-import io.element.android.libraries.usersearch.test.FakeUserRepository
-import io.element.android.tests.testutils.WarmUpRule
-import io.element.android.tests.testutils.consumeItemsUntilPredicate
-import io.element.android.tests.testutils.testCoroutineDispatchers
-import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.persistentListOf
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Rule
-import org.junit.Test
-
-internal class RoomInviteMembersPresenterTest {
- @get:Rule
- val warmUpRule = WarmUpRule()
-
- @Test
- fun `present - initial state has no results and no search`() = runTest {
- val presenter = RoomInviteMembersPresenter(
- userRepository = FakeUserRepository(),
- roomMemberListDataSource = createDataSource(FakeBaseRoom()),
- coroutineDispatchers = testCoroutineDispatchers()
- )
-
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
-
- assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
- assertThat(initialState.isSearchActive).isFalse()
- assertThat(initialState.canInvite).isFalse()
- assertThat(initialState.searchQuery).isEmpty()
-
- skipItems(1)
- }
- }
-
- @Test
- fun `present - updates search active state`() = runTest {
- val presenter = RoomInviteMembersPresenter(
- userRepository = FakeUserRepository(),
- roomMemberListDataSource = createDataSource(FakeBaseRoom()),
- coroutineDispatchers = testCoroutineDispatchers()
- )
-
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- skipItems(1)
-
- initialState.eventSink(RoomInviteMembersEvents.OnSearchActiveChanged(true))
-
- val resultState = awaitItem()
- assertThat(resultState.isSearchActive).isTrue()
- }
- }
-
- @Test
- fun `present - performs search and handles empty result list`() = runTest {
- val repository = FakeUserRepository()
- val presenter = RoomInviteMembersPresenter(
- userRepository = repository,
- roomMemberListDataSource = createDataSource(FakeBaseRoom()),
- coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
- )
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query"))
- assertThat(repository.providedQuery).isEqualTo("some query")
- repository.emitState(UserSearchResultState(results = emptyList(), isSearching = true))
- consumeItemsUntilPredicate { it.showSearchLoader }.last().also { state ->
- assertThat(state.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
- assertThat(state.showSearchLoader).isTrue()
- }
- repository.emitState(results = emptyList(), isSearching = false)
- consumeItemsUntilPredicate { !it.showSearchLoader }.last().also { state ->
- assertThat(state.searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
- assertThat(state.showSearchLoader).isFalse()
- }
- }
- }
-
- @Test
- fun `present - performs search and handles user results`() = runTest {
- val repository = FakeUserRepository()
- val presenter = RoomInviteMembersPresenter(
- userRepository = repository,
- roomMemberListDataSource = createDataSource(FakeBaseRoom()),
- coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
- )
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- skipItems(1)
-
- initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query"))
- skipItems(1)
-
- assertThat(repository.providedQuery).isEqualTo("some query")
- repository.emitStateWithUsers(users = aMatrixUserList())
- skipItems(1)
-
- val resultState = awaitItem()
- assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
-
- val expectedUsers = aMatrixUserList()
- val users = resultState.searchResults.users()
- expectedUsers.forEachIndexed { index, matrixUser ->
- assertThat(users[index].matrixUser).isEqualTo(matrixUser)
- assertThat(users[index].isAlreadyInvited).isFalse()
- assertThat(users[index].isAlreadyJoined).isFalse()
- assertThat(users[index].isSelected).isFalse()
- }
- }
- }
-
- @Test
- fun `present - performs search and handles membership state of existing users`() = runTest {
- val userList = aMatrixUserList()
- val joinedUser = userList[0]
- val invitedUser = userList[1]
-
- val repository = FakeUserRepository()
- val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
- val presenter = RoomInviteMembersPresenter(
- userRepository = repository,
- roomMemberListDataSource = createDataSource(
- room = FakeBaseRoom().apply {
- givenRoomMembersState(
- RoomMembersState.Ready(
- persistentListOf(
- aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN),
- aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE),
- )
- )
- )
- },
- coroutineDispatchers = coroutineDispatchers,
- ),
- coroutineDispatchers = coroutineDispatchers
- )
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- skipItems(1)
-
- initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query"))
- skipItems(1)
-
- assertThat(repository.providedQuery).isEqualTo("some query")
- repository.emitStateWithUsers(users = aMatrixUserList())
- skipItems(1)
-
- val resultState = awaitItem()
- assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
-
- val users = resultState.searchResults.users()
-
- // The result that matches a user with JOINED membership is marked as such
- val userWhoShouldBeJoined = users.find { it.matrixUser == joinedUser }
- assertThat(userWhoShouldBeJoined).isNotNull()
- assertThat(userWhoShouldBeJoined?.isAlreadyJoined).isTrue()
- assertThat(userWhoShouldBeJoined?.isAlreadyInvited).isFalse()
-
- // The result that matches a user with INVITED membership is marked as such
- val userWhoShouldBeInvited = users.find { it.matrixUser == invitedUser }
- assertThat(userWhoShouldBeInvited).isNotNull()
- assertThat(userWhoShouldBeInvited?.isAlreadyJoined).isFalse()
- assertThat(userWhoShouldBeInvited?.isAlreadyInvited).isTrue()
-
- // All other users are neither joined nor invited
- val otherUsers = users.minus(userWhoShouldBeInvited!!).minus(userWhoShouldBeJoined!!)
- assertThat(otherUsers.none { it.isAlreadyInvited }).isTrue()
- assertThat(otherUsers.none { it.isAlreadyJoined }).isTrue()
- }
- }
-
- @Test
- fun `present - performs search and handles unresolved results`() = runTest {
- val userList = aMatrixUserList()
- val joinedUser = userList[0]
- val invitedUser = userList[1]
-
- val repository = FakeUserRepository()
- val presenter = RoomInviteMembersPresenter(
- userRepository = repository,
- roomMemberListDataSource = createDataSource(FakeBaseRoom().apply {
- givenRoomMembersState(
- RoomMembersState.Ready(
- persistentListOf(
- aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN),
- aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE),
- )
- )
- )
- }),
- coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
- )
-
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- skipItems(1)
-
- initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query"))
- skipItems(1)
-
- assertThat(repository.providedQuery).isEqualTo("some query")
-
- val unresolvedUser = UserSearchResult(aMatrixUser(id = A_USER_ID.value), isUnresolved = true)
- repository.emitState(listOf(unresolvedUser) + aMatrixUserList().map { UserSearchResult(it) })
- skipItems(1)
-
- val resultState = awaitItem()
- assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
-
- val users = resultState.searchResults.users()
-
- val userWhoShouldBeUnresolved = users.first()
- assertThat(userWhoShouldBeUnresolved.isUnresolved).isTrue()
-
- // All other users are neither joined nor invited
- val otherUsers = users.minus(userWhoShouldBeUnresolved)
- assertThat(otherUsers.none { it.isUnresolved }).isTrue()
- }
- }
-
- @Test
- fun `present - toggle users updates selected user state`() = runTest {
- val repository = FakeUserRepository()
- val presenter = RoomInviteMembersPresenter(
- userRepository = repository,
- roomMemberListDataSource = createDataSource(),
- coroutineDispatchers = testCoroutineDispatchers()
- )
-
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- skipItems(1)
-
- // When we toggle a user not in the list, they are added
- initialState.eventSink(RoomInviteMembersEvents.ToggleUser(aMatrixUser()))
- assertThat(awaitItem().selectedUsers).containsExactly(aMatrixUser())
-
- // Toggling a different user also adds them
- initialState.eventSink(RoomInviteMembersEvents.ToggleUser(aMatrixUser(id = A_USER_ID_2.value)))
- assertThat(awaitItem().selectedUsers).containsExactly(aMatrixUser(), aMatrixUser(id = A_USER_ID_2.value))
-
- // Toggling the first user removes them
- initialState.eventSink(RoomInviteMembersEvents.ToggleUser(aMatrixUser()))
- assertThat(awaitItem().selectedUsers).containsExactly(aMatrixUser(id = A_USER_ID_2.value))
- }
- }
-
- @Test
- fun `present - selected users appear as such in search results`() = runTest {
- val repository = FakeUserRepository()
- val presenter = RoomInviteMembersPresenter(
- userRepository = repository,
- roomMemberListDataSource = createDataSource(FakeBaseRoom()),
- coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
- )
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- skipItems(1)
-
- val selectedUser = aMatrixUser()
-
- initialState.eventSink(RoomInviteMembersEvents.ToggleUser(selectedUser))
-
- initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query"))
- skipItems(1)
-
- assertThat(repository.providedQuery).isEqualTo("some query")
- repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
- skipItems(2)
-
- val resultState = awaitItem()
- assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
-
- val users = resultState.searchResults.users()
-
- // The one user we have previously toggled is marked as selected
- val shouldBeSelectedUser = users.find { it.matrixUser == selectedUser }
- assertThat(shouldBeSelectedUser).isNotNull()
- assertThat(shouldBeSelectedUser?.isSelected).isTrue()
-
- // And no others are
- val allOtherUsers = users.minus(shouldBeSelectedUser!!)
- assertThat(allOtherUsers.none { it.isSelected }).isTrue()
- }
- }
-
- @Test
- fun `present - toggling a user updates existing search results`() = runTest {
- val repository = FakeUserRepository()
- val presenter = RoomInviteMembersPresenter(
- userRepository = repository,
- roomMemberListDataSource = createDataSource(FakeBaseRoom()),
- coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
- )
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- skipItems(1)
-
- val selectedUser = aMatrixUser()
-
- // Given a query is made
- initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query"))
- skipItems(1)
-
- assertThat(repository.providedQuery).isEqualTo("some query")
- repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
- skipItems(2)
-
- // And then a user is toggled
- initialState.eventSink(RoomInviteMembersEvents.ToggleUser(selectedUser))
- skipItems(1)
- val resultState = awaitItem()
-
- // The results are updated...
- assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
- val users = resultState.searchResults.users()
-
- // The one user we have now toggled is marked as selected
- val shouldBeSelectedUser = users.find { it.matrixUser == selectedUser }
- assertThat(shouldBeSelectedUser).isNotNull()
- assertThat(shouldBeSelectedUser?.isSelected).isTrue()
-
- // And no others are
- val allOtherUsers = users.minus(shouldBeSelectedUser!!)
- assertThat(allOtherUsers.none { it.isSelected }).isTrue()
- }
- }
-
- private suspend fun FakeUserRepository.emitStateWithUsers(
- users: List,
- isSearching: Boolean = false
- ) {
- emitState(
- results = users.map { UserSearchResult(it) },
- isSearching = isSearching,
- )
- }
-
- private suspend fun FakeUserRepository.emitState(
- results: List,
- isSearching: Boolean = false
- ) {
- val state = UserSearchResultState(
- results = results,
- isSearching = isSearching
- )
- emitState(state)
- }
-
- private fun TestScope.createDataSource(
- room: BaseRoom = aRoom().apply {
- givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
- },
- coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers()
- ) = RoomMemberListDataSource(room, coroutineDispatchers)
-
- private fun SearchBarResultState>.users() =
- (this as? SearchBarResultState.Results>)?.results.orEmpty()
-}
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 c49ae2d4d5..a7f61e1a55 100644
--- a/features/roommembermoderation/impl/src/main/res/values-eu/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-eu/translations.xml
@@ -4,6 +4,7 @@
"Ezarri debekua"
"Ziur kide honi debekua ezarri nahi diozula?"
"%1$s(r)i debekua ezartzen"
+ "Kendu"
"Ikusi profila"
"Kendu gelatik"
"Kidea kendu eta etorkizunean sartzea debekatu?"
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 152e2d3a8f..1f2e372b8c 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
@@ -12,4 +12,9 @@
"踢出聊天室"
"移除成員並禁止未來再度加入?"
"正在踢出 %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 20c002aa76..3eb55ef12b 100644
--- a/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml
+++ b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml
@@ -12,4 +12,7 @@
"从聊天室移除"
"删除成员并禁止重新加入?"
"正在移除 %1$s……"
+ "解除封禁"
+ "如果再次收到邀请,他们可以重新加入该聊天室"
+ "确定要解除该成员的封禁吗?"
diff --git a/features/securebackup/impl/src/main/res/values-eu/translations.xml b/features/securebackup/impl/src/main/res/values-eu/translations.xml
index 3cb643a04c..139127f8c7 100644
--- a/features/securebackup/impl/src/main/res/values-eu/translations.xml
+++ b/features/securebackup/impl/src/main/res/values-eu/translations.xml
@@ -16,6 +16,7 @@
"Gorde berreskuratze-gako berria pasahitz-kudeatzaile batean edo enkriptatutako ohar batean"
"Berrezarri zure kontuaren enkriptazioa beste gailu bat erabiliz"
"Jarraitu berrezarpenarekin"
+ "Zure kontuaren xehetasunak, kontaktuak, hobespenak eta txat-zerrenda gordeko dira"
"Ezin duzu baieztatu? Zure identitatea berrezarri beharko duzu."
"Desaktibatu"
"Enkriptatutako mezuak galduko dituzu gailu guztietan saioa amaitzen baduzu."
diff --git a/features/securebackup/impl/src/main/res/values-ur/translations.xml b/features/securebackup/impl/src/main/res/values-ur/translations.xml
index 80b55d8878..f1098de9dc 100644
--- a/features/securebackup/impl/src/main/res/values-ur/translations.xml
+++ b/features/securebackup/impl/src/main/res/values-ur/translations.xml
@@ -16,6 +16,7 @@
"نئی بازیابی کلید بنانے کیلئے ہدایات پر عمل کریں"
"اپنی نئی بازیابی کلید کو لفظ عبور منتظم یا مرکوز کردہ ملحوظہ میں محفوظ کریں"
"کسی دوسرے آلے کا استعمال کرتے ہوئے اپنے کھاتہ کیلئے مرموز کاری کو بحال کریں"
+ "ری سیٹ جاری رکھیں"
"آپ کے کھاتہ کی تفصیلات، رابطے، ترجیحات اور گفتگو کی فہرست رکھی جائے گی۔"
"آپ کسی بھی پیغام کی سرگزشت کو کھو دیں گے جو صرف خادم پر محفوظ ہے۔"
"آپ کو اپنے تمام موجودہ آلات اور رابطوں کی دوبارہ توثیق کرنی ہوگی۔"
@@ -57,6 +58,7 @@
"ہاں، اب بحال کر دیں"
"یہ عملیہ ناقابل تلافی ہے۔"
"کیا آپ کو یقین ہے کہ آپ اپنی شناخت بحال کر دینا چاہتے ہیں؟"
+ "ایک نامعلوم غلطی ہو گئی۔ براہ کرم چیک کریں کہ آپ کے اکاؤنٹ کا پاسورڈ درست ہے اور دوبارہ کوشش کریں۔"
"درج کریں…"
"تصدیق کریں کہ آپ اپنی شناخت بحال کر دینا چاہتے ہیں۔"
"جاری رکھنے کے لیے اپنے کھاتہ کا لفظ عبور درج کریں۔"
diff --git a/features/securebackup/impl/src/main/res/values-uz/translations.xml b/features/securebackup/impl/src/main/res/values-uz/translations.xml
index c3bec705ad..9f2cb7f163 100644
--- a/features/securebackup/impl/src/main/res/values-uz/translations.xml
+++ b/features/securebackup/impl/src/main/res/values-uz/translations.xml
@@ -2,7 +2,7 @@
"Zaxiralashni o\'chirib qo\'ying"
"Zaxiralashni yoqing"
- "Zaxiralash xabarlar tarixini yo\'qotmaslikni ta\'minlaydi.%1$s."
+ "Kryptografik shaxsiyatingizni va xabar kalitlaringizni serverda xavfsiz saqlang. Bu sizga har qanday yangi qurilmalarda xabar tarixingizni ko\'rish imkonini beradi. %1$s."
"Zaxira"
"Qayta tiklash kalitini o\'zgartiring"
"Sizning chat zaxirangiz hozirda sinxronlashtirilmagan."
@@ -17,13 +17,15 @@
"Haqiqatan ham zaxiralashni o‘chirib qo‘ymoqchimisiz?"
"Mavjud kalitingizni yo\'qotgan bo\'lsangiz, yangi tiklash kalitini oling. Qayta tiklash kalitini almashtirganingizdan so\'ng, eski kalitingiz ishlamaydi."
"Yangi tiklash kalitini yarating"
- "Qayta tiklash kalitingizni xavfsiz joyda saqlashingiz mumkinligiga ishonch hosil qiling"
+ "Buni hech kimga ulashmang!"
"Qayta tiklash kaliti oʻzgartirildi"
"Qayta tiklash kaliti almashtirilsinmi?"
"Hech kim bu ekranni kora olmasligiga ishonch hosil qiling!"
"Agar sizda xavfsizlik kaliti yoki xavfsizlik iborasi bolsa, bu ham ishlaydi."
"Kirish…"
"Qayta tiklash kaliti tasdiqlandi"
+ "Qayta tiklash kaliti nusxalandi"
+ "Yaratilmoqda…"
"Qayta tiklash kalitini saqlang"
"Qayta tiklash kalitingizni xavfsiz joyga yozing yoki parol menejerida saqlang."
"Qayta tiklash kalitidan nusxa olish uchun bosing"
@@ -32,7 +34,7 @@
"Zaxira kalitingizni saqladingizmi?"
"Suhbatingiz zaxira nusxasi tiklash kaliti bilan himoyalangan. Agar sozlashdan keyin sizga yangi tiklash kaliti kerak boʻlsa, “Qayta tiklash kalitini oʻzgartirish”ni tanlash orqali qayta yaratishingiz mumkin."
"Qayta tiklash kalitini yarating"
- "Qayta tiklash kalitingizni xavfsiz joyda saqlashingiz mumkinligiga ishonch hosil qiling"
+ "Buni hech kimga ulashmang!"
"Qayta tiklash muvaffaqiyatli sozlandi"
"Qayta tiklashni sozlang"
"Kirish…"
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 ea38b0bc70..b66765f472 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
@@ -22,6 +22,7 @@ import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaSender
@@ -88,6 +89,7 @@ class SharePresenter @AssistedInject constructor(
val mediaSender = MediaSender(
preProcessor = mediaPreProcessor,
room = room,
+ timelineMode = Timeline.Mode.Live,
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
)
filesToShare
diff --git a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt
index a9346fbf6e..7ddb7d0ae4 100644
--- a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt
+++ b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt
@@ -17,7 +17,6 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.core.mimetype.MimeTypes
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.ProgressCallback
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_ROOM_ID
@@ -122,7 +121,7 @@ class SharePresenterTest {
@Test
fun `present - send media ok`() = runTest {
val sendFileResult =
- lambdaRecorder> { _, _, _, _, _, _ ->
+ lambdaRecorder> { _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
val joinedRoom = FakeJoinedRoom(
diff --git a/features/startchat/api/build.gradle.kts b/features/startchat/api/build.gradle.kts
new file mode 100644
index 0000000000..77822f1a15
--- /dev/null
+++ b/features/startchat/api/build.gradle.kts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+plugins {
+ id("io.element.android-library")
+}
+
+android {
+ namespace = "io.element.android.features.startchat.api"
+}
+
+dependencies {
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.matrix.api)
+}
diff --git a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/ConfirmingStartDmWithMatrixUser.kt b/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/ConfirmingStartDmWithMatrixUser.kt
similarity index 89%
rename from features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/ConfirmingStartDmWithMatrixUser.kt
rename to features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/ConfirmingStartDmWithMatrixUser.kt
index af19408324..192c32fee7 100644
--- a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/ConfirmingStartDmWithMatrixUser.kt
+++ b/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/ConfirmingStartDmWithMatrixUser.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.api
+package io.element.android.features.startchat.api
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.user.MatrixUser
diff --git a/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/StartChatEntryPoint.kt b/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/StartChatEntryPoint.kt
new file mode 100644
index 0000000000..17b9b902e2
--- /dev/null
+++ b/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/StartChatEntryPoint.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.features.startchat.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.RoomIdOrAlias
+
+interface StartChatEntryPoint : FeatureEntryPoint {
+ fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
+ interface NodeBuilder {
+ fun callback(callback: Callback): NodeBuilder
+ fun build(): Node
+ }
+
+ interface Callback : Plugin {
+ fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List)
+ fun onOpenRoomDirectory()
+ }
+}
diff --git a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/StartDMAction.kt b/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/StartDMAction.kt
similarity index 95%
rename from features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/StartDMAction.kt
rename to features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/StartDMAction.kt
index e64be9f923..b49a9fbdfd 100644
--- a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/StartDMAction.kt
+++ b/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/StartDMAction.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.api
+package io.element.android.features.startchat.api
import androidx.compose.runtime.MutableState
import io.element.android.libraries.architecture.AsyncAction
diff --git a/features/startchat/impl/build.gradle.kts b/features/startchat/impl/build.gradle.kts
new file mode 100644
index 0000000000..ddc8a271ef
--- /dev/null
+++ b/features/startchat/impl/build.gradle.kts
@@ -0,0 +1,65 @@
+import extension.ComponentMergingStrategy
+import extension.setupAnvil
+
+/*
+ * Copyright 2022-2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+plugins {
+ id("io.element.android-compose-library")
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = "io.element.android.features.startchat.impl"
+
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+}
+
+setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
+
+dependencies {
+ implementation(projects.libraries.core)
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.matrix.api)
+ implementation(projects.libraries.matrixui)
+ implementation(projects.libraries.designsystem)
+ implementation(projects.libraries.uiStrings)
+ implementation(projects.libraries.androidutils)
+ implementation(projects.libraries.deeplink.api)
+ implementation(projects.libraries.mediapickers.api)
+ implementation(projects.libraries.mediaupload.api)
+ implementation(projects.libraries.permissions.api)
+ implementation(projects.libraries.usersearch.impl)
+ implementation(projects.services.analytics.api)
+ implementation(libs.coil.compose)
+ implementation(projects.libraries.featureflag.api)
+ implementation(projects.features.createroom.api)
+ api(projects.features.startchat.api)
+
+ testImplementation(libs.test.junit)
+ testImplementation(libs.test.mockk)
+ testImplementation(libs.coroutines.test)
+ testImplementation(libs.molecule.runtime)
+ testImplementation(libs.test.truth)
+ testImplementation(libs.test.turbine)
+ testImplementation(libs.test.robolectric)
+ testImplementation(projects.services.analytics.test)
+ testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.libraries.mediapickers.test)
+ testImplementation(projects.libraries.mediaupload.test)
+ testImplementation(projects.libraries.permissions.test)
+ testImplementation(projects.libraries.usersearch.test)
+ testImplementation(projects.features.startchat.test)
+ testImplementation(projects.libraries.featureflag.test)
+ testImplementation(projects.tests.testutils)
+ testImplementation(libs.androidx.compose.ui.test.junit)
+ testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
+}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/CreateRoomNavigator.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/StartChatNavigator.kt
similarity index 86%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/CreateRoomNavigator.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/StartChatNavigator.kt
index 69eac7d369..a45b4dddab 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/CreateRoomNavigator.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/StartChatNavigator.kt
@@ -5,18 +5,18 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom
+package io.element.android.features.startchat
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
-import io.element.android.features.createroom.impl.CreateRoomFlowNode.NavTarget
+import io.element.android.features.startchat.impl.StartChatFlowNode.NavTarget
import io.element.android.libraries.architecture.overlay.Overlay
import io.element.android.libraries.architecture.overlay.operation.hide
import io.element.android.libraries.architecture.overlay.operation.show
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
-interface CreateRoomNavigator : Plugin {
+interface StartChatNavigator : Plugin {
fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List)
fun onCreateNewRoom()
fun onShowJoinRoomByAddress()
@@ -24,12 +24,12 @@ interface CreateRoomNavigator : Plugin {
fun onOpenRoomDirectory()
}
-class DefaultCreateRoomNavigator(
+class DefaultStartChatNavigator(
private val backstack: BackStack,
private val overlay: Overlay,
private val openRoom: (RoomIdOrAlias, List) -> Unit,
private val openRoomDirectory: () -> Unit,
-) : CreateRoomNavigator {
+) : StartChatNavigator {
override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List) = openRoom(roomIdOrAlias, serverNames)
override fun onOpenRoomDirectory() = openRoomDirectory()
diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartChatEntryPoint.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartChatEntryPoint.kt
new file mode 100644
index 0000000000..9f9073f1d7
--- /dev/null
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartChatEntryPoint.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.startchat.impl
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.startchat.api.StartChatEntryPoint
+import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.di.AppScope
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultStartChatEntryPoint @Inject constructor() : StartChatEntryPoint {
+ override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): StartChatEntryPoint.NodeBuilder {
+ val plugins = ArrayList()
+
+ return object : StartChatEntryPoint.NodeBuilder {
+ override fun callback(callback: StartChatEntryPoint.Callback): StartChatEntryPoint.NodeBuilder {
+ plugins += callback
+ return this
+ }
+
+ override fun build(): Node {
+ return parentNode.createNode(buildContext, plugins)
+ }
+ }
+ }
+}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultStartDMAction.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt
similarity index 90%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultStartDMAction.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt
index c9b60786bd..b09c5ea174 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultStartDMAction.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt
@@ -5,13 +5,13 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl
+package io.element.android.features.startchat.impl
import androidx.compose.runtime.MutableState
import com.squareup.anvil.annotations.ContributesBinding
import im.vector.app.features.analytics.plan.CreatedRoom
-import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
-import io.element.android.features.createroom.api.StartDMAction
+import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
+import io.element.android.features.startchat.api.StartDMAction
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
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
new file mode 100644
index 0000000000..a312b6fa84
--- /dev/null
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/StartChatFlowNode.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.startchat.impl
+
+import android.os.Parcelable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.navigation.transition.JumpToEndTransitionHandler
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.bumble.appyx.core.plugin.plugins
+import com.bumble.appyx.navmodel.backstack.BackStack
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.features.createroom.api.CreateRoomEntryPoint
+import io.element.android.features.startchat.DefaultStartChatNavigator
+import io.element.android.features.startchat.api.StartChatEntryPoint
+import io.element.android.features.startchat.impl.joinbyaddress.JoinRoomByAddressNode
+import io.element.android.features.startchat.impl.root.StartChatNode
+import io.element.android.libraries.architecture.BackstackView
+import io.element.android.libraries.architecture.BaseFlowNode
+import io.element.android.libraries.architecture.OverlayView
+import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
+import kotlinx.parcelize.Parcelize
+
+@ContributesNode(SessionScope::class)
+class StartChatFlowNode @AssistedInject constructor(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ private val createRoomEntryPoint: CreateRoomEntryPoint,
+) : BaseFlowNode(
+ backstack = BackStack(
+ initialElement = NavTarget.Root,
+ savedStateMap = buildContext.savedStateMap,
+ ),
+ buildContext = buildContext,
+ plugins = plugins
+) {
+ sealed interface NavTarget : Parcelable {
+ @Parcelize
+ data object Root : NavTarget
+
+ @Parcelize
+ data object NewRoom : NavTarget
+
+ @Parcelize
+ data object JoinByAddress : NavTarget
+ }
+
+ private val navigator = DefaultStartChatNavigator(
+ backstack = backstack,
+ overlay = overlay,
+ openRoom = { roomIdOrAlias, viaServers ->
+ plugins().forEach { it.onOpenRoom(roomIdOrAlias, viaServers) }
+ },
+ openRoomDirectory = {
+ plugins().forEach { it.onOpenRoomDirectory() }
+ }
+ )
+
+ override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
+ return when (navTarget) {
+ NavTarget.Root -> {
+ createNode(buildContext = buildContext, plugins = listOf(navigator))
+ }
+ NavTarget.NewRoom -> {
+ val callback = object : CreateRoomEntryPoint.Callback {
+ override fun onRoomCreated(roomId: RoomId) {
+ navigator.onOpenRoom(roomId.toRoomIdOrAlias(), emptyList())
+ }
+ }
+ createRoomEntryPoint.nodeBuilder(parentNode = this, buildContext = buildContext)
+ .callback(callback)
+ .build()
+ }
+ NavTarget.JoinByAddress -> {
+ createNode(buildContext = buildContext, plugins = listOf(navigator))
+ }
+ }
+ }
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ Box(modifier = modifier) {
+ BackstackView()
+ OverlayView(transitionHandler = remember { JumpToEndTransitionHandler() })
+ }
+ }
+}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/SearchMultipleUsersResultItem.kt
similarity index 98%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/SearchMultipleUsersResultItem.kt
index af12895fff..867ac9d918 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/SearchMultipleUsersResultItem.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.components
+package io.element.android.features.startchat.impl.components
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/SearchSingleUserResultItem.kt
similarity index 97%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/SearchSingleUserResultItem.kt
index eb922f1ae3..bd94d136af 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/SearchSingleUserResultItem.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.components
+package io.element.android.features.startchat.impl.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/SearchUserBar.kt
similarity index 98%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/SearchUserBar.kt
index 290c1aff0c..3d42c40067 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/SearchUserBar.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.components
+package io.element.android.features.startchat.impl.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/UserListView.kt
similarity index 94%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/UserListView.kt
index a660480258..753bebec83 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/UserListView.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.components
+package io.element.android.features.startchat.impl.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -16,9 +16,9 @@ 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.features.createroom.impl.userlist.UserListEvents
-import io.element.android.features.createroom.impl.userlist.UserListState
-import io.element.android.features.createroom.impl.userlist.UserListStateProvider
+import io.element.android.features.startchat.impl.userlist.UserListEvents
+import io.element.android.features.startchat.impl.userlist.UserListState
+import io.element.android.features.startchat.impl.userlist.UserListStateProvider
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressEvents.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressEvents.kt
similarity index 86%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressEvents.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressEvents.kt
index dbbcef56fa..648146fbd7 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressEvents.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressEvents.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.joinbyaddress
+package io.element.android.features.startchat.impl.joinbyaddress
sealed interface JoinRoomByAddressEvents {
data object Dismiss : JoinRoomByAddressEvents
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressNode.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressNode.kt
similarity index 85%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressNode.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressNode.kt
index d6338ab43c..67dba8b46e 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressNode.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressNode.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.joinbyaddress
+package io.element.android.features.startchat.impl.joinbyaddress
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -16,7 +16,7 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.createroom.CreateRoomNavigator
+import io.element.android.features.startchat.StartChatNavigator
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@@ -25,7 +25,7 @@ class JoinRoomByAddressNode @AssistedInject constructor(
@Assisted plugins: List,
presenterFactory: JoinRoomByAddressPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
- private val navigator = plugins().first()
+ private val navigator = plugins().first()
private val presenter = presenterFactory.create(navigator)
@Composable
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressPresenter.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressPresenter.kt
similarity index 95%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressPresenter.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressPresenter.kt
index cf4788cbbf..4e1f9f9ab0 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressPresenter.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressPresenter.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.joinbyaddress
+package io.element.android.features.startchat.impl.joinbyaddress
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -18,7 +18,7 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
-import io.element.android.features.createroom.CreateRoomNavigator
+import io.element.android.features.startchat.StartChatNavigator
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.MatrixClient
@@ -32,13 +32,13 @@ import kotlin.time.Duration.Companion.seconds
private const val ADDRESS_RESOLVE_TIMEOUT_IN_SECONDS = 10
class JoinRoomByAddressPresenter @AssistedInject constructor(
- @Assisted private val navigator: CreateRoomNavigator,
+ @Assisted private val navigator: StartChatNavigator,
private val client: MatrixClient,
private val roomAliasHelper: RoomAliasHelper,
) : Presenter {
@AssistedFactory
interface Factory {
- fun create(navigator: CreateRoomNavigator): JoinRoomByAddressPresenter
+ fun create(navigator: StartChatNavigator): JoinRoomByAddressPresenter
}
@Composable
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressState.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressState.kt
similarity index 92%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressState.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressState.kt
index 11791181e1..84b01ac263 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressState.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressState.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.joinbyaddress
+package io.element.android.features.startchat.impl.joinbyaddress
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt
similarity index 95%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt
index 6281a8e8e3..847ca78837 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.joinbyaddress
+package io.element.android.features.startchat.impl.joinbyaddress
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomId
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressView.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt
similarity index 97%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressView.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt
index c256f5140c..2d34dcc1cf 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressView.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.joinbyaddress
+package io.element.android.features.startchat.impl.joinbyaddress
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -29,7 +29,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
-import io.element.android.features.createroom.impl.R
+import io.element.android.features.startchat.impl.R
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatEvents.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatEvents.kt
new file mode 100644
index 0000000000..6ea72d8b05
--- /dev/null
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatEvents.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.startchat.impl.root
+
+import io.element.android.libraries.matrix.api.user.MatrixUser
+
+sealed interface StartChatEvents {
+ data class StartDM(val matrixUser: MatrixUser) : StartChatEvents
+ data object CancelStartDM : StartChatEvents
+}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatNode.kt
similarity index 83%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatNode.kt
index f76a5d5c61..9b9a1ec0dc 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatNode.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.root
+package io.element.android.features.startchat.impl.root
import android.app.Activity
import androidx.activity.compose.LocalActivity
@@ -20,21 +20,21 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.createroom.CreateRoomNavigator
-import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
+import io.element.android.features.startchat.StartChatNavigator
+import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(SessionScope::class)
-class CreateRoomRootNode @AssistedInject constructor(
+class StartChatNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- private val presenter: CreateRoomRootPresenter,
+ private val presenter: StartChatPresenter,
private val analyticsService: AnalyticsService,
private val inviteFriendsUseCase: InviteFriendsUseCase,
) : Node(buildContext, plugins = plugins) {
- private val navigator = plugins().first()
+ private val navigator = plugins().first()
init {
lifecycle.subscribe(
@@ -46,7 +46,7 @@ class CreateRoomRootNode @AssistedInject constructor(
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = requireNotNull(LocalActivity.current)
- CreateRoomRootView(
+ StartChatView(
state = state,
modifier = modifier,
onCloseClick = this::navigateUp,
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt
similarity index 73%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt
index ea8be45e6b..9f7bea5c68 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.root
+package io.element.android.features.startchat.impl.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
@@ -14,11 +14,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
-import io.element.android.features.createroom.api.StartDMAction
-import io.element.android.features.createroom.impl.userlist.SelectionMode
-import io.element.android.features.createroom.impl.userlist.UserListDataStore
-import io.element.android.features.createroom.impl.userlist.UserListPresenter
-import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
+import io.element.android.features.startchat.api.StartDMAction
+import io.element.android.features.startchat.impl.userlist.SelectionMode
+import io.element.android.features.startchat.impl.userlist.UserListDataStore
+import io.element.android.features.startchat.impl.userlist.UserListPresenter
+import io.element.android.features.startchat.impl.userlist.UserListPresenterArgs
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
@@ -29,14 +29,14 @@ import io.element.android.libraries.usersearch.api.UserRepository
import kotlinx.coroutines.launch
import javax.inject.Inject
-class CreateRoomRootPresenter @Inject constructor(
+class StartChatPresenter @Inject constructor(
presenterFactory: UserListPresenter.Factory,
userRepository: UserRepository,
userListDataStore: UserListDataStore,
private val startDMAction: StartDMAction,
private val buildMeta: BuildMeta,
private val featureFlagService: FeatureFlagService,
-) : Presenter {
+) : Presenter {
private val presenter = presenterFactory.create(
UserListPresenterArgs(
selectionMode = SelectionMode.Single,
@@ -46,7 +46,7 @@ class CreateRoomRootPresenter @Inject constructor(
)
@Composable
- override fun present(): CreateRoomRootState {
+ override fun present(): StartChatState {
val userListState = presenter.present()
val localCoroutineScope = rememberCoroutineScope()
@@ -56,20 +56,20 @@ class CreateRoomRootPresenter @Inject constructor(
featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch)
}.collectAsState(initial = false)
- fun handleEvents(event: CreateRoomRootEvents) {
+ fun handleEvents(event: StartChatEvents) {
when (event) {
- is CreateRoomRootEvents.StartDM -> localCoroutineScope.launch {
+ is StartChatEvents.StartDM -> localCoroutineScope.launch {
startDMAction.execute(
matrixUser = event.matrixUser,
createIfDmDoesNotExist = startDmActionState.value is AsyncAction.Confirming,
actionState = startDmActionState,
)
}
- CreateRoomRootEvents.CancelStartDM -> startDmActionState.value = AsyncAction.Uninitialized
+ StartChatEvents.CancelStartDM -> startDmActionState.value = AsyncAction.Uninitialized
}
}
- return CreateRoomRootState(
+ return StartChatState(
applicationName = buildMeta.applicationName,
userListState = userListState,
startDmAction = startDmActionState.value,
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt
similarity index 63%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt
index 7a6d651db3..724d6e5e88 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt
@@ -1,20 +1,20 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.root
+package io.element.android.features.startchat.impl.root
-import io.element.android.features.createroom.impl.userlist.UserListState
+import io.element.android.features.startchat.impl.userlist.UserListState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
-data class CreateRoomRootState(
+data class StartChatState(
val applicationName: String,
val userListState: UserListState,
val startDmAction: AsyncAction,
val isRoomDirectorySearchEnabled: Boolean,
- val eventSink: (CreateRoomRootEvents) -> Unit,
+ val eventSink: (StartChatEvents) -> Unit,
)
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt
similarity index 80%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt
index 9f2d59e3b6..6b5113cc94 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt
@@ -1,17 +1,17 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.root
+package io.element.android.features.startchat.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
-import io.element.android.features.createroom.impl.userlist.UserListState
-import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList
-import io.element.android.features.createroom.impl.userlist.aUserListState
+import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
+import io.element.android.features.startchat.impl.userlist.UserListState
+import io.element.android.features.startchat.impl.userlist.aRecentDirectRoomList
+import io.element.android.features.startchat.impl.userlist.aUserListState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomId
@@ -19,8 +19,8 @@ import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.collections.immutable.persistentListOf
-open class CreateRoomRootStateProvider : PreviewParameterProvider {
- override val values: Sequence
+open class StartChatStateProvider : PreviewParameterProvider {
+ override val values: Sequence
get() = sequenceOf(
aCreateRoomRootState(),
aCreateRoomRootState(
@@ -64,8 +64,8 @@ fun aCreateRoomRootState(
userListState: UserListState = aUserListState(),
startDmAction: AsyncAction = AsyncAction.Uninitialized,
isRoomDirectorySearchEnabled: Boolean = false,
- eventSink: (CreateRoomRootEvents) -> Unit = {},
-) = CreateRoomRootState(
+ eventSink: (StartChatEvents) -> Unit = {},
+) = StartChatState(
applicationName = applicationName,
userListState = userListState,
startDmAction = startDmAction,
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt
similarity index 89%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt
index 248d331985..ebde080f75 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.root
+package io.element.android.features.startchat.impl.root
import androidx.annotation.DrawableRes
import androidx.compose.foundation.clickable
@@ -27,9 +27,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
-import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
-import io.element.android.features.createroom.impl.R
-import io.element.android.features.createroom.impl.components.UserListView
+import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
+import io.element.android.features.startchat.impl.R
+import io.element.android.features.startchat.impl.components.UserListView
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.button.BackButton
@@ -48,8 +48,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentListOf
@Composable
-fun CreateRoomRootView(
- state: CreateRoomRootState,
+fun StartChatView(
+ state: StartChatState,
onCloseClick: () -> Unit,
onNewRoomClick: () -> Unit,
onOpenDM: (RoomId) -> Unit,
@@ -80,7 +80,7 @@ fun CreateRoomRootView(
recentDirectRooms = persistentListOf(),
),
onSelectUser = {
- state.eventSink(CreateRoomRootEvents.StartDM(it))
+ state.eventSink(StartChatEvents.StartDM(it))
},
onDeselectUser = { },
)
@@ -109,20 +109,20 @@ fun CreateRoomRootView(
errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) },
onRetry = {
state.userListState.selectedUsers.firstOrNull()
- ?.let { state.eventSink(CreateRoomRootEvents.StartDM(it)) }
+ ?.let { state.eventSink(StartChatEvents.StartDM(it)) }
// Cancel start DM if there is no more selected user (should not happen)
- ?: state.eventSink(CreateRoomRootEvents.CancelStartDM)
+ ?: state.eventSink(StartChatEvents.CancelStartDM)
},
- onErrorDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) },
+ onErrorDismiss = { state.eventSink(StartChatEvents.CancelStartDM) },
confirmationDialog = { data ->
if (data is ConfirmingStartDmWithMatrixUser) {
CreateDmConfirmationBottomSheet(
matrixUser = data.matrixUser,
onSendInvite = {
- state.eventSink(CreateRoomRootEvents.StartDM(data.matrixUser))
+ state.eventSink(StartChatEvents.StartDM(data.matrixUser))
},
onDismiss = {
- state.eventSink(CreateRoomRootEvents.CancelStartDM)
+ state.eventSink(StartChatEvents.CancelStartDM)
},
)
}
@@ -148,7 +148,7 @@ private fun CreateRoomRootViewTopBar(
@Composable
private fun CreateRoomActionButtonsList(
- state: CreateRoomRootState,
+ state: StartChatState,
onNewRoomClick: () -> Unit,
onInvitePeopleClick: () -> Unit,
onJoinByAddressClick: () -> Unit,
@@ -239,9 +239,9 @@ private fun CreateRoomActionButton(
@PreviewsDayNight
@Composable
-internal fun CreateRoomRootViewPreview(@PreviewParameter(CreateRoomRootStateProvider::class) state: CreateRoomRootState) =
+internal fun StartChatViewPreview(@PreviewParameter(StartChatStateProvider::class) state: StartChatState) =
ElementPreview {
- CreateRoomRootView(
+ StartChatView(
state = state,
onCloseClick = {},
onNewRoomClick = {},
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt
similarity index 98%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt
index 32d5767cc6..c964b18441 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.userlist
+package io.element.android.features.startchat.impl.userlist
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListDataStore.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListDataStore.kt
similarity index 93%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListDataStore.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListDataStore.kt
index a500e3a05f..64048d7e86 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListDataStore.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListDataStore.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.userlist
+package io.element.android.features.startchat.impl.userlist
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.coroutines.flow.MutableStateFlow
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListEvents.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListEvents.kt
similarity index 90%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListEvents.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListEvents.kt
index 45b40597da..794b2c18af 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListEvents.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListEvents.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.userlist
+package io.element.android.features.startchat.impl.userlist
import io.element.android.libraries.matrix.api.user.MatrixUser
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListPresenter.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListPresenter.kt
similarity index 90%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListPresenter.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListPresenter.kt
index 38f45d202f..a07b39a1e7 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListPresenter.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListPresenter.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.userlist
+package io.element.android.features.startchat.impl.userlist
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.usersearch.api.UserRepository
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListPresenterArgs.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListPresenterArgs.kt
similarity index 84%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListPresenterArgs.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListPresenterArgs.kt
index 85cc58e995..d7e6a727f8 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListPresenterArgs.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListPresenterArgs.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.userlist
+package io.element.android.features.startchat.impl.userlist
data class UserListPresenterArgs(
val selectionMode: SelectionMode,
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListState.kt
similarity index 94%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListState.kt
index bed3ac7f63..2186bcb57a 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListState.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.userlist
+package io.element.android.features.startchat.impl.userlist
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListStateProvider.kt
similarity index 98%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt
rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListStateProvider.kt
index 3b5918abc5..65b4efdadb 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt
+++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListStateProvider.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.userlist
+package io.element.android.features.startchat.impl.userlist
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
diff --git a/features/startchat/impl/src/main/res/values-be/translations.xml b/features/startchat/impl/src/main/res/values-be/translations.xml
new file mode 100644
index 0000000000..106159a00d
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-be/translations.xml
@@ -0,0 +1,6 @@
+
+
+ "Новы пакой"
+ "Каталог пакояў"
+ "Пры спробе пачаць чат адбылася памылка"
+
diff --git a/features/startchat/impl/src/main/res/values-bg/translations.xml b/features/startchat/impl/src/main/res/values-bg/translations.xml
new file mode 100644
index 0000000000..21ad117fb6
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-bg/translations.xml
@@ -0,0 +1,9 @@
+
+
+ "Нова стая"
+ "Присъединяване към стая по адрес"
+ "Не е валиден адрес"
+ "Въведете…"
+ "Стаята не е намерена"
+ "напр. #room-name:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-cs/translations.xml b/features/startchat/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..b89f27fb9e
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Nová místnost"
+ "Adresář místností"
+ "Při pokusu o zahájení chatu došlo k chybě"
+ "Vstoupit do místnosti pomocí adresy"
+ "Neplatná adresa"
+ "Zadejte…"
+ "Odpovídající místnost nalezena"
+ "Místnost nebyla nalezena"
+ "např. #room-name:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-cy/translations.xml b/features/startchat/impl/src/main/res/values-cy/translations.xml
new file mode 100644
index 0000000000..47faa4c5c9
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-cy/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Ystafell newydd"
+ "Cyfeiriadur ystafelloedd"
+ "Digwyddodd gwall wrth geisio cychwyn sgwrs"
+ "Ymuno â\'r ystafell yn ôl cyfeiriad"
+ "Ddim yn gyfeiriad dilys"
+ "Ewch i mewn…"
+ "Cafwyd hyd i ystafell gyfatebol"
+ "Heb ganfod yr ystafell"
+ "e.e. #enw-ystafell:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-da/translations.xml b/features/startchat/impl/src/main/res/values-da/translations.xml
new file mode 100644
index 0000000000..89092045f5
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-da/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Nyt rum"
+ "Register over rum"
+ "Der opstod en fejl under forsøget på at starte en samtale"
+ "Tilslut dig rummet med adressen"
+ "Ikke en gyldig adresse"
+ "Indtast…"
+ "Matchende rum fundet"
+ "Rum ikke fundet"
+ "f.eks. #rummets-navn:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-de/translations.xml b/features/startchat/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..dff0a6fdea
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Neuer Raum"
+ "Raum-Verzeichnis"
+ "Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"
+ "Raum per Adresse betreten"
+ "Keine gültige Adresse"
+ "Eintreten…"
+ "Passender Raum gefunden"
+ "Raum nicht gefunden"
+ "z. B. #room -name:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-el/translations.xml b/features/startchat/impl/src/main/res/values-el/translations.xml
new file mode 100644
index 0000000000..19e6250d12
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-el/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Νέα αίθουσα"
+ "Κατάλογος αιθουσών"
+ "Παρουσιάστηκε σφάλμα κατά την προσπάθεια έναρξης μιας συνομιλίας"
+ "Συμμετοχή σε αίθουσα μέσω διεύθυνσης"
+ "Μη έγκυρη διεύθυνση"
+ "Εισάγετε…"
+ "Βρέθηκε η αντίστοιχη αίθουσα"
+ "Η αίθουσα δεν βρέθηκε"
+ "π.χ. #όνομα-αίθουσας:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-es/translations.xml b/features/startchat/impl/src/main/res/values-es/translations.xml
new file mode 100644
index 0000000000..64a9f02a17
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-es/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Nueva sala"
+ "Directorio de salas"
+ "Se ha producido un error al intentar iniciar un chat"
+ "Unirse a una sala por su dirección"
+ "Dirección no válida"
+ "Introducir…"
+ "Sala encontrada"
+ "No se encontró la sala"
+ "p. ej., #nombre-de-la-sala:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-et/translations.xml b/features/startchat/impl/src/main/res/values-et/translations.xml
new file mode 100644
index 0000000000..65459475a8
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-et/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Uus jututuba"
+ "Jututubade kataloog"
+ "Vestluse alustamisel tekkis viga"
+ "Liitu jututoaga aadressi alusel"
+ "See pole kehtiv aadress"
+ "Sisene…"
+ "Leidsime vastava jututoa"
+ "Jututuba ei leidu"
+ "nt. #jututoa-nimi:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-eu/translations.xml b/features/startchat/impl/src/main/res/values-eu/translations.xml
new file mode 100644
index 0000000000..403db09c9d
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-eu/translations.xml
@@ -0,0 +1,9 @@
+
+
+ "Gela berria"
+ "Gelen direktorioa"
+ "Errorea gertatu da txata hasten saiatzean"
+ "Ez da baliozko helbidea"
+ "Sartu…"
+ "Ez da gela aurkitu"
+
diff --git a/features/startchat/impl/src/main/res/values-fa/translations.xml b/features/startchat/impl/src/main/res/values-fa/translations.xml
new file mode 100644
index 0000000000..7cb1e1baee
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-fa/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "اتاق جدید"
+ "فهرست اتاقها"
+ "هنگام تلاش برای شروع چت خطایی روی داد"
+ "پیوستن به اتاق با نشانی"
+ "نشانی معتبری نیست"
+ "ورود…"
+ "اتاق مطابق پیدا شد"
+ "اتاق پیدا نشد"
+ "نمونه: #room-name:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-fi/translations.xml b/features/startchat/impl/src/main/res/values-fi/translations.xml
new file mode 100644
index 0000000000..13659571c8
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-fi/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Uusi huone"
+ "Huoneluettelo"
+ "Keskustelun aloituksessa tapahtui virhe"
+ "Liity huoneeseen osoitteella"
+ "Osoite ei ole kelvollinen"
+ "Syötä…"
+ "Täsmäävä huone löytyi"
+ "Huonetta ei löytynyt"
+ "esim. #huoneen-nimi:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-fr/translations.xml b/features/startchat/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..9aaebd018f
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Nouveau salon"
+ "Annuaire des salons"
+ "Une erreur s’est produite lors de la tentative de création de la discussion"
+ "Saisir une adresse de salon"
+ "Ce n’est pas une adresse valide"
+ "Saisir…"
+ "Ce salon existe"
+ "Salon non trouvé"
+ "ex: #nom-du-salon:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-hu/translations.xml b/features/startchat/impl/src/main/res/values-hu/translations.xml
new file mode 100644
index 0000000000..014f7baaac
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-hu/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Új szoba"
+ "Szobakatalógus"
+ "Hiba történt a csevegés indításakor"
+ "Csatlakozás a szobához cím szerint"
+ "Nem érvényes cím"
+ "Írja be…"
+ "Megfelelő szoba található"
+ "Szoba nem található"
+ "pl. #szoba-neve:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-in/translations.xml b/features/startchat/impl/src/main/res/values-in/translations.xml
new file mode 100644
index 0000000000..5f796bb8da
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-in/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Ruangan baru"
+ "Direktori ruangan"
+ "Terjadi kesalahan saat mencoba memulai obrolan"
+ "Bergabung dalam ruangan berdasarkan alamat"
+ "Bukan alamat yang valid"
+ "Masuk…"
+ "Ruangan yang cocok ditemukan"
+ "Ruangan tidak ditemukan"
+ "mis. #nama-ruangan:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-it/translations.xml b/features/startchat/impl/src/main/res/values-it/translations.xml
new file mode 100644
index 0000000000..94818d7ab1
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-it/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Nuova stanza"
+ "Elenco delle stanze"
+ "Si è verificato un errore durante il tentativo di avviare una chat"
+ "Accedi alla stanza tramite indirizzo"
+ "Indirizzo non valido"
+ "Inserisci…"
+ "Stanza trovata"
+ "Stanza non trovata"
+ "ad esempio #room -name:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-ka/translations.xml b/features/startchat/impl/src/main/res/values-ka/translations.xml
new file mode 100644
index 0000000000..c5ccd79305
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-ka/translations.xml
@@ -0,0 +1,6 @@
+
+
+ "ახალი ოთახი"
+ "ოთახის კატალოგი"
+ "ჩატის დაწყების მცდელობისას შეცდომა მოხდა"
+
diff --git a/features/startchat/impl/src/main/res/values-lt/translations.xml b/features/startchat/impl/src/main/res/values-lt/translations.xml
new file mode 100644
index 0000000000..699e0703f1
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-lt/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Naujas kambarys"
+ "Bandant pradėti pokalbį įvyko klaida"
+
diff --git a/features/startchat/impl/src/main/res/values-nb/translations.xml b/features/startchat/impl/src/main/res/values-nb/translations.xml
new file mode 100644
index 0000000000..ffd8fe9cfc
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-nb/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Nytt rom"
+ "Romkatalog"
+ "Det oppstod en feil når du prøvde å starte en chat"
+ "Bli med i rommet med adresse"
+ "Ikke en gyldig adresse"
+ "Gå inn…"
+ "Matchende rom funnet"
+ "Rom ikke funnet"
+ "f.eks. #rom-navn:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-nl/translations.xml b/features/startchat/impl/src/main/res/values-nl/translations.xml
new file mode 100644
index 0000000000..244ffddba3
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-nl/translations.xml
@@ -0,0 +1,6 @@
+
+
+ "Nieuwe kamer"
+ "Kamergids"
+ "Er is een fout opgetreden bij het starten van een chat"
+
diff --git a/features/startchat/impl/src/main/res/values-pl/translations.xml b/features/startchat/impl/src/main/res/values-pl/translations.xml
new file mode 100644
index 0000000000..fe6b4a2c8d
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-pl/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Nowy pokój"
+ "Katalog pokoi"
+ "Wystąpił błąd podczas próby rozpoczęcia czatu"
+ "Dołącz do pokoju za pomocą adresu"
+ "Nieprawidłowy adres"
+ "Wprowadź…"
+ "Znaleziono pasujący pokój"
+ "Nie znaleziono pokoju"
+ "np. #room-name:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-pt-rBR/translations.xml b/features/startchat/impl/src/main/res/values-pt-rBR/translations.xml
new file mode 100644
index 0000000000..f17991c56b
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-pt-rBR/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Nova sala"
+ "Diretório de salas"
+ "Ocorreu um erro ao tentar iniciar um chat"
+ "Entrar na sala pelo endereço"
+ "Não é um endereço válido"
+ "Entrar…"
+ "Foi encontrada uma sala correspondente"
+ "Sala não encontrada"
+ "Por exemplo, #nome-da-sala:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-pt/translations.xml b/features/startchat/impl/src/main/res/values-pt/translations.xml
new file mode 100644
index 0000000000..3efcbf7640
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-pt/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Nova sala"
+ "Diretório de salas"
+ "Ocorreu um erro ao tentar iniciar uma conversa"
+ "Entrar na sala pelo endereço"
+ "Não é um endereço válido"
+ "Entrar…"
+ "Sala correspondente encontrado"
+ "Sala não encontrada"
+ "por exemplo, #sala:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-ro/translations.xml b/features/startchat/impl/src/main/res/values-ro/translations.xml
new file mode 100644
index 0000000000..d306d83aab
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-ro/translations.xml
@@ -0,0 +1,6 @@
+
+
+ "Cameră nouă"
+ "Director de camere"
+ "A apărut o eroare la încercarea începerii conversației"
+
diff --git a/features/startchat/impl/src/main/res/values-ru/translations.xml b/features/startchat/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..6d676d3c76
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Создать новую комнату"
+ "Каталог комнат"
+ "Произошла ошибка при запуске чата"
+ "Присоединиться к комнате по адресу"
+ "Недействительный адрес"
+ "Ввести…"
+ "Соответствующая комната найдена"
+ "Комната не найдена"
+ "прим. #room-name:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-sk/translations.xml b/features/startchat/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..16a1548765
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Nová miestnosť"
+ "Adresár miestností"
+ "Pri pokuse o spustenie konverzácie sa vyskytla chyba"
+ "Pripojte sa do miestnosti podľa adresy"
+ "Neplatná adresa"
+ "Zadajte…"
+ "Nájdená zodpovedajúca miestnosť"
+ "Miestnosť sa nenašla"
+ "napr. #nazov-miestnosti:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-sv/translations.xml b/features/startchat/impl/src/main/res/values-sv/translations.xml
new file mode 100644
index 0000000000..21cfb470b1
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-sv/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Nytt rum"
+ "Rumskatalog"
+ "Ett fel uppstod när du försökte starta en chatt"
+ "Gå med i rum med adress"
+ "Inte en giltig adress"
+ "Ange …"
+ "Matchande rum hittades"
+ "Rummet hittades inte"
+ "t.ex. #rumsnamn:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-tr/translations.xml b/features/startchat/impl/src/main/res/values-tr/translations.xml
new file mode 100644
index 0000000000..581996500d
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-tr/translations.xml
@@ -0,0 +1,6 @@
+
+
+ "Yeni oda"
+ "Oda dizini"
+ "Sohbet başlatmaya çalışırken bir hata oluştu"
+
diff --git a/features/startchat/impl/src/main/res/values-uk/translations.xml b/features/startchat/impl/src/main/res/values-uk/translations.xml
new file mode 100644
index 0000000000..3c2b09939c
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-uk/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "Нова кімната"
+ "Каталог кімнат"
+ "Під час спроби почати бесіду сталася помилка"
+ "Приєднатися до кімнати за адресою"
+ "Недійсна адреса"
+ "Введіть…"
+ "Знайдено відповідну кімнату"
+ "Кімната не знайдена"
+ "наприклад, #room-name:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-ur/translations.xml b/features/startchat/impl/src/main/res/values-ur/translations.xml
new file mode 100644
index 0000000000..394f6aa817
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-ur/translations.xml
@@ -0,0 +1,6 @@
+
+
+ "نیا کمرہ"
+ "کمرے کا راہنامچہ"
+ "گفتگو شروع کرنے کی کوشش کرتے وقت ایک خرابی واقع ہوگئی"
+
diff --git a/features/startchat/impl/src/main/res/values-uz/translations.xml b/features/startchat/impl/src/main/res/values-uz/translations.xml
new file mode 100644
index 0000000000..6f68899a3b
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-uz/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Yangi xona"
+ "Suhbatni boshlashda xatolik yuz berdi"
+
diff --git a/features/startchat/impl/src/main/res/values-zh-rTW/translations.xml b/features/startchat/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..c6dcd50008
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,12 @@
+
+
+ "建立聊天室"
+ "聊天室目錄"
+ "嘗試開始聊天時發生錯誤"
+ "按地址加入聊天室"
+ "不是有效的位址"
+ "輸入……"
+ "找到相符的聊天室"
+ "找不到聊天室"
+ "例如 #room-name:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values-zh/translations.xml b/features/startchat/impl/src/main/res/values-zh/translations.xml
new file mode 100644
index 0000000000..2f80b65973
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values-zh/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "新聊天室"
+ "聊天室目录"
+ "在开始聊天时发生了错误"
+ "输入地址加入房间"
+ "地址无效"
+ "输入…"
+ "未找到房间"
+ "例如 #room-name:matrix.org"
+
diff --git a/features/startchat/impl/src/main/res/values/localazy.xml b/features/startchat/impl/src/main/res/values/localazy.xml
new file mode 100644
index 0000000000..48b6449263
--- /dev/null
+++ b/features/startchat/impl/src/main/res/values/localazy.xml
@@ -0,0 +1,12 @@
+
+
+ "New room"
+ "Room directory"
+ "An error occurred when trying to start a chat"
+ "Join room by address"
+ "Not a valid address"
+ "Enter…"
+ "Matching room found"
+ "Room not found"
+ "e.g. #room-name:matrix.org"
+
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultStartDMActionTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt
similarity index 97%
rename from features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultStartDMActionTest.kt
rename to features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt
index ef6b3b9517..fa6e140c3a 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultStartDMActionTest.kt
+++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt
@@ -5,12 +5,12 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl
+package io.element.android.features.startchat.impl
import androidx.compose.runtime.mutableStateOf
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.CreatedRoom
-import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
+import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/FakeCreateRoomNavigator.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/FakeStartChatNavigator.kt
similarity index 86%
rename from features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/FakeCreateRoomNavigator.kt
rename to features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/FakeStartChatNavigator.kt
index 28dcdd409d..9de00e0a4c 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/FakeCreateRoomNavigator.kt
+++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/FakeStartChatNavigator.kt
@@ -5,18 +5,18 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl
+package io.element.android.features.startchat.impl
-import io.element.android.features.createroom.CreateRoomNavigator
+import io.element.android.features.startchat.StartChatNavigator
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
-class FakeCreateRoomNavigator(
+class FakeStartChatNavigator(
private val openRoomLambda: (roomIdOrAlias: RoomIdOrAlias, serverNames: List) -> Unit = { _, _ -> },
private val createNewRoomLambda: () -> Unit = {},
private val showJoinRoomByAddressLambda: () -> Unit = {},
private val dismissJoinRoomByAddressLambda: () -> Unit = {},
private val openRoomDirectoryLambda: () -> Unit = {},
-) : CreateRoomNavigator {
+) : StartChatNavigator {
override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List) {
openRoomLambda(roomIdOrAlias, serverNames)
}
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressPresenterTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressPresenterTest.kt
similarity index 93%
rename from features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressPresenterTest.kt
rename to features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressPresenterTest.kt
index 10cca20fa3..bd40b92d57 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressPresenterTest.kt
+++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressPresenterTest.kt
@@ -5,11 +5,11 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.joinbyaddress
+package io.element.android.features.startchat.impl.joinbyaddress
import com.google.common.truth.Truth.assertThat
-import io.element.android.features.createroom.CreateRoomNavigator
-import io.element.android.features.createroom.impl.FakeCreateRoomNavigator
+import io.element.android.features.startchat.StartChatNavigator
+import io.element.android.features.startchat.impl.FakeStartChatNavigator
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
@@ -61,7 +61,7 @@ class JoinBaseRoomByAddressPresenterTest {
fun `present - room found`() = runTest {
val openRoomLambda = lambdaRecorder, Unit> { _, _ -> }
val dismissJoinRoomByAddressLambda = lambdaRecorder { }
- val navigator = FakeCreateRoomNavigator(
+ val navigator = FakeStartChatNavigator(
openRoomLambda = openRoomLambda,
dismissJoinRoomByAddressLambda = dismissJoinRoomByAddressLambda
)
@@ -114,7 +114,7 @@ class JoinBaseRoomByAddressPresenterTest {
@Test
fun `present - dismiss`() = runTest {
val dismissJoinRoomByAddressLambda = lambdaRecorder { }
- val navigator = FakeCreateRoomNavigator(
+ val navigator = FakeStartChatNavigator(
dismissJoinRoomByAddressLambda = dismissJoinRoomByAddressLambda
)
val presenter = createJoinRoomByAddressPresenter(navigator = navigator)
@@ -127,7 +127,7 @@ class JoinBaseRoomByAddressPresenterTest {
}
private fun createJoinRoomByAddressPresenter(
- navigator: CreateRoomNavigator = FakeCreateRoomNavigator(),
+ navigator: StartChatNavigator = FakeStartChatNavigator(),
matrixClient: MatrixClient = FakeMatrixClient(),
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
): JoinRoomByAddressPresenter {
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt
similarity index 94%
rename from features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt
rename to features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt
index b2e75c9f5a..4de9bd1470 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt
+++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.joinbyaddress
+package io.element.android.features.startchat.impl.joinbyaddress
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
@@ -13,7 +13,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
-import io.element.android.features.createroom.impl.R
+import io.element.android.features.startchat.impl.R
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootPresenterTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt
similarity index 83%
rename from features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootPresenterTest.kt
rename to features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt
index d2d9959e2a..59a8838c6b 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootPresenterTest.kt
+++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt
@@ -1,23 +1,23 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.root
+package io.element.android.features.startchat.impl.root
import androidx.compose.runtime.MutableState
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
-import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
-import io.element.android.features.createroom.api.StartDMAction
-import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter
-import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
-import io.element.android.features.createroom.impl.userlist.UserListDataStore
-import io.element.android.features.createroom.test.FakeStartDMAction
+import io.element.android.features.invitepeople.test.FakeStartDMAction
+import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
+import io.element.android.features.startchat.api.StartDMAction
+import io.element.android.features.startchat.impl.userlist.FakeUserListPresenter
+import io.element.android.features.startchat.impl.userlist.FakeUserListPresenterFactory
+import io.element.android.features.startchat.impl.userlist.UserListDataStore
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
@@ -36,7 +36,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
-class CreateBaseRoomRootPresenterTest {
+class StartChatPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@@ -47,7 +47,7 @@ class CreateBaseRoomRootPresenterTest {
actionState.value = startDMFailureResult
}
val startDMAction = FakeStartDMAction(executeResult = executeResult)
- val presenter = createCreateRoomRootPresenter(startDMAction)
+ val presenter = createStartChatPresenter(startDMAction)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -58,7 +58,7 @@ class CreateBaseRoomRootPresenterTest {
assertThat(initialState.userListState.isSearchActive).isFalse()
assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse()
val matrixUser = MatrixUser(UserId("@name:domain"))
- initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
+ initialState.eventSink(StartChatEvents.StartDM(matrixUser))
awaitItem().also { state ->
assertThat(state.startDmAction).isEqualTo(startDMFailureResult)
executeResult.assertions().isCalledOnce().with(
@@ -66,7 +66,7 @@ class CreateBaseRoomRootPresenterTest {
value(false),
any(),
)
- state.eventSink(CreateRoomRootEvents.CancelStartDM)
+ state.eventSink(StartChatEvents.CancelStartDM)
}
awaitItem().also { state ->
assertThat(state.startDmAction.isUninitialized()).isTrue()
@@ -81,7 +81,7 @@ class CreateBaseRoomRootPresenterTest {
actionState.value = startDMSuccessResult
}
val startDMAction = FakeStartDMAction(executeResult = executeResult)
- val presenter = createCreateRoomRootPresenter(startDMAction)
+ val presenter = createStartChatPresenter(startDMAction)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -92,7 +92,7 @@ class CreateBaseRoomRootPresenterTest {
assertThat(initialState.userListState.isSearchActive).isFalse()
assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse()
val matrixUser = MatrixUser(UserId("@name:domain"))
- initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
+ initialState.eventSink(StartChatEvents.StartDM(matrixUser))
awaitItem().also { state ->
assertThat(state.startDmAction).isEqualTo(startDMSuccessResult)
executeResult.assertions().isCalledOnce().with(
@@ -112,13 +112,13 @@ class CreateBaseRoomRootPresenterTest {
actionState.value = startDMConfirmationResult
}
val startDMAction = FakeStartDMAction(executeResult = executeResult)
- val presenter = createCreateRoomRootPresenter(startDMAction)
+ val presenter = createStartChatPresenter(startDMAction)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
- initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
+ initialState.eventSink(StartChatEvents.StartDM(matrixUser))
val confirmingState = awaitItem()
assertThat(confirmingState.startDmAction).isEqualTo(startDMConfirmationResult)
executeResult.assertions().isCalledOnce().with(
@@ -127,7 +127,7 @@ class CreateBaseRoomRootPresenterTest {
any(),
)
// Cancelling should not create the DM
- confirmingState.eventSink(CreateRoomRootEvents.CancelStartDM)
+ confirmingState.eventSink(StartChatEvents.CancelStartDM)
val finalState = awaitItem()
assertThat(finalState.startDmAction.isUninitialized()).isTrue()
executeResult.assertions().isCalledExactly(1)
@@ -142,13 +142,13 @@ class CreateBaseRoomRootPresenterTest {
actionState.value = startDMConfirmationResult
}
val startDMAction = FakeStartDMAction(executeResult = executeResult)
- val presenter = createCreateRoomRootPresenter(startDMAction)
+ val presenter = createStartChatPresenter(startDMAction)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
- initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
+ initialState.eventSink(StartChatEvents.StartDM(matrixUser))
val confirmingState = awaitItem()
assertThat(confirmingState.startDmAction).isEqualTo(startDMConfirmationResult)
executeResult.assertions().isCalledOnce().with(
@@ -157,7 +157,7 @@ class CreateBaseRoomRootPresenterTest {
any(),
)
// Start DM again should invoke the action with createIfDmDoesNotExist = true
- confirmingState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
+ confirmingState.eventSink(StartChatEvents.StartDM(matrixUser))
executeResult.assertions().isCalledExactly(2).withSequence(
listOf(value(matrixUser), value(false), any()),
listOf(value(matrixUser), value(true), any()),
@@ -167,7 +167,7 @@ class CreateBaseRoomRootPresenterTest {
@Test
fun `present - room directory search`() = runTest {
- val presenter = createCreateRoomRootPresenter(isRoomDirectorySearchEnabled = true)
+ val presenter = createStartChatPresenter(isRoomDirectorySearchEnabled = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -178,16 +178,16 @@ class CreateBaseRoomRootPresenterTest {
}
}
- private fun createCreateRoomRootPresenter(
+ private fun createStartChatPresenter(
startDMAction: StartDMAction = FakeStartDMAction(),
isRoomDirectorySearchEnabled: Boolean = false,
- ): CreateRoomRootPresenter {
+ ): StartChatPresenter {
val featureFlagService = FakeFeatureFlagService(
initialState = mapOf(
FeatureFlags.RoomDirectorySearch.key to isRoomDirectorySearchEnabled,
),
)
- return CreateRoomRootPresenter(
+ return StartChatPresenter(
presenterFactory = FakeUserListPresenterFactory(FakeUserListPresenter()),
userRepository = FakeUserRepository(),
userListDataStore = UserListDataStore(),
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootViewTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt
similarity index 80%
rename from features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootViewTest.kt
rename to features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt
index 9104b2dfb2..dc213446a0 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootViewTest.kt
+++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.root
+package io.element.android.features.startchat.impl.root
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
@@ -13,9 +13,9 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
-import io.element.android.features.createroom.impl.R
-import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList
-import io.element.android.features.createroom.impl.userlist.aUserListState
+import io.element.android.features.startchat.impl.R
+import io.element.android.features.startchat.impl.userlist.aRecentDirectRoomList
+import io.element.android.features.startchat.impl.userlist.aUserListState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.ui.strings.CommonStrings
@@ -33,15 +33,15 @@ import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
-class CreateBaseRoomRootViewTest {
+class StartChatViewTest {
@get:Rule
val rule = createAndroidComposeRule()
@Test
fun `clicking on back invokes the expected callback`() {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnce {
- rule.setCreateRoomRootView(
+ rule.setStartChatView(
aCreateRoomRootState(
eventSink = eventsRecorder,
),
@@ -53,9 +53,9 @@ class CreateBaseRoomRootViewTest {
@Test
fun `clicking on New room invokes the expected callback`() {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnce {
- rule.setCreateRoomRootView(
+ rule.setStartChatView(
aCreateRoomRootState(
eventSink = eventsRecorder,
),
@@ -68,9 +68,9 @@ class CreateBaseRoomRootViewTest {
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on Invite people invokes the expected callback`() {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnce {
- rule.setCreateRoomRootView(
+ rule.setStartChatView(
aCreateRoomRootState(
applicationName = "test",
eventSink = eventsRecorder,
@@ -87,9 +87,9 @@ class CreateBaseRoomRootViewTest {
fun `clicking on a user suggestion invokes the expected callback`() {
val recentDirectRoomList = aRecentDirectRoomList()
val firstRoom = recentDirectRoomList[0]
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnceWithParam(firstRoom.roomId) {
- rule.setCreateRoomRootView(
+ rule.setStartChatView(
aCreateRoomRootState(
userListState = aUserListState(
recentDirectRooms = recentDirectRoomList
@@ -105,9 +105,9 @@ class CreateBaseRoomRootViewTest {
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on Join room by address invokes the expected callback`() {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnce {
- rule.setCreateRoomRootView(
+ rule.setStartChatView(
aCreateRoomRootState(
eventSink = eventsRecorder,
),
@@ -119,9 +119,9 @@ class CreateBaseRoomRootViewTest {
@Test
fun `clicking on room directory invokes the expected callback`() {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnce {
- rule.setCreateRoomRootView(
+ rule.setStartChatView(
aCreateRoomRootState(
eventSink = eventsRecorder,
isRoomDirectorySearchEnabled = true
@@ -133,8 +133,8 @@ class CreateBaseRoomRootViewTest {
}
}
-private fun AndroidComposeTestRule.setCreateRoomRootView(
- state: CreateRoomRootState,
+private fun AndroidComposeTestRule.setStartChatView(
+ state: StartChatState,
onCloseClick: () -> Unit = EnsureNeverCalled(),
onNewRoomClick: () -> Unit = EnsureNeverCalled(),
onOpenDM: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
@@ -143,7 +143,7 @@ private fun AndroidComposeTestRule.setCreat
onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
- CreateRoomRootView(
+ StartChatView(
state = state,
onCloseClick = onCloseClick,
onNewRoomClick = onNewRoomClick,
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenterTest.kt
similarity index 99%
rename from features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTest.kt
rename to features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenterTest.kt
index ac793a247f..50bbc425c2 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTest.kt
+++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenterTest.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.userlist
+package io.element.android.features.startchat.impl.userlist
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/FakeUserListPresenter.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/FakeUserListPresenter.kt
similarity index 89%
rename from features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/FakeUserListPresenter.kt
rename to features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/FakeUserListPresenter.kt
index a0768ffda8..84c4fd9d67 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/FakeUserListPresenter.kt
+++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/FakeUserListPresenter.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.userlist
+package io.element.android.features.startchat.impl.userlist
import androidx.compose.runtime.Composable
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/FakeUserListPresenterFactory.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/FakeUserListPresenterFactory.kt
similarity index 91%
rename from features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/FakeUserListPresenterFactory.kt
rename to features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/FakeUserListPresenterFactory.kt
index 123a013670..8e80626e86 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/FakeUserListPresenterFactory.kt
+++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/FakeUserListPresenterFactory.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.userlist
+package io.element.android.features.startchat.impl.userlist
import io.element.android.libraries.usersearch.api.UserRepository
diff --git a/features/createroom/test/build.gradle.kts b/features/startchat/test/build.gradle.kts
similarity index 83%
rename from features/createroom/test/build.gradle.kts
rename to features/startchat/test/build.gradle.kts
index b7df0caab6..96b05a80fa 100644
--- a/features/createroom/test/build.gradle.kts
+++ b/features/startchat/test/build.gradle.kts
@@ -10,7 +10,7 @@ plugins {
}
android {
- namespace = "io.element.android.features.createroom.test"
+ namespace = "io.element.android.features.invitepeople.test"
}
dependencies {
@@ -19,5 +19,5 @@ dependencies {
implementation(projects.libraries.matrix.test)
implementation(projects.libraries.architecture)
implementation(projects.tests.testutils)
- api(projects.features.createroom.api)
+ api(projects.features.startchat.api)
}
diff --git a/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/test/FakeStartDMAction.kt b/features/startchat/test/src/main/kotlin/io/element/android/features/invitepeople/test/FakeStartDMAction.kt
similarity index 88%
rename from features/createroom/test/src/main/kotlin/io/element/android/features/createroom/test/FakeStartDMAction.kt
rename to features/startchat/test/src/main/kotlin/io/element/android/features/invitepeople/test/FakeStartDMAction.kt
index 90e2ecf1c1..d7ab63d0e3 100644
--- a/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/test/FakeStartDMAction.kt
+++ b/features/startchat/test/src/main/kotlin/io/element/android/features/invitepeople/test/FakeStartDMAction.kt
@@ -5,10 +5,10 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.test
+package io.element.android.features.invitepeople.test
import androidx.compose.runtime.MutableState
-import io.element.android.features.createroom.api.StartDMAction
+import io.element.android.features.startchat.api.StartDMAction
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
diff --git a/features/userprofile/impl/build.gradle.kts b/features/userprofile/impl/build.gradle.kts
index 4242efcb98..7bcebae565 100644
--- a/features/userprofile/impl/build.gradle.kts
+++ b/features/userprofile/impl/build.gradle.kts
@@ -38,7 +38,7 @@ dependencies {
api(projects.features.userprofile.api)
api(projects.features.userprofile.shared)
implementation(libs.coil.compose)
- implementation(projects.features.createroom.api)
+ implementation(projects.features.startchat.api)
implementation(projects.services.analytics.api)
testImplementation(libs.test.junit)
@@ -49,7 +49,7 @@ dependencies {
testImplementation(libs.test.mockk)
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.matrix.test)
- testImplementation(projects.features.createroom.test)
+ testImplementation(projects.features.startchat.test)
testImplementation(projects.features.enterprise.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit)
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 4de79b8375..b7fae6082b 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
@@ -20,8 +20,8 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
-import io.element.android.features.createroom.api.StartDMAction
import io.element.android.features.enterprise.api.SessionEnterpriseService
+import io.element.android.features.startchat.api.StartDMAction
import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.features.userprofile.api.UserProfileState.ConfirmationDialog
diff --git a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt
index 2ce7f99c2c..dd937b067a 100644
--- a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt
+++ b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt
@@ -13,10 +13,10 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
-import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
-import io.element.android.features.createroom.api.StartDMAction
-import io.element.android.features.createroom.test.FakeStartDMAction
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
+import io.element.android.features.invitepeople.test.FakeStartDMAction
+import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
+import io.element.android.features.startchat.api.StartDMAction
import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.features.userprofile.api.UserProfileVerificationState
diff --git a/features/userprofile/shared/build.gradle.kts b/features/userprofile/shared/build.gradle.kts
index 0b14ce63f2..c49b78866f 100644
--- a/features/userprofile/shared/build.gradle.kts
+++ b/features/userprofile/shared/build.gradle.kts
@@ -1,5 +1,3 @@
-import extension.setupAnvil
-
/*
* Copyright 2024 New Vector Ltd.
*
@@ -21,8 +19,6 @@ android {
}
}
-setupAnvil()
-
dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
@@ -39,7 +35,7 @@ dependencies {
api(projects.features.userprofile.api)
api(projects.services.apperror.api)
implementation(libs.coil.compose)
- implementation(projects.features.createroom.api)
+ implementation(projects.features.startchat.api)
implementation(projects.services.analytics.api)
testImplementation(libs.test.junit)
diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt
index 7a5cc53239..25cb8a5df6 100644
--- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt
+++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt
@@ -8,7 +8,7 @@
package io.element.android.features.userprofile.shared
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
+import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.features.userprofile.api.UserProfileVerificationState
diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt
index a43478e466..f39cbfaf73 100644
--- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt
+++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt
@@ -21,7 +21,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
-import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
+import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.features.userprofile.api.UserProfileVerificationState
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 fceaef0572..922cc8e193 100644
--- a/features/verifysession/impl/src/main/res/values-zh/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-zh/translations.xml
@@ -39,9 +39,13 @@
"匹配"
"从此处开始验证之前,请确保您已在其他设备上打开了该应用程序。"
"在另一台验证的设备上打开应用"
+ "为了提高安全性,请通过比较设备上的一组表情符号来验证此用户。通过使用安全方式来做到这一点,如面对面。"
+ "验证此用户?"
+ "为了提高安全性,另一位用户想要验证您的身份。您将看到一组表情符号供您比较。"
"您应该会在另一台设备上看到一个弹出窗口。现在从那里开始验证。"
"在另一台设备上开始验证"
"正在等待其他设备"
+ "等待其他用户"
"请在其他会话中接受验证请求。"
"等待接受请求"
"正在登出…"
diff --git a/gradle.properties b/gradle.properties
index 74dc7941d2..38cf7488a4 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -41,7 +41,7 @@ signing.element.nightly.keyPassword=Secret
# Customise the Lint version to use a more recent version than the one bundled with AGP
# https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html
-android.experimental.lint.version=8.12.0
+android.experimental.lint.version=8.12.1
# Enable test fixture for all modules by default
android.experimental.enableTestFixtures=true
@@ -54,3 +54,6 @@ com.squareup.anvil.kspContributingAnnotations=io.element.android.anvilannotation
# Only apply KSP to main sources
ksp.allow.all.target.configuration=false
+
+# Used to prevent detekt from reusing invalid cached rules
+detekt.use.worker.api=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ae05a0cd2e..f9cc0c07cc 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,14 +3,15 @@
[versions]
# Project
-android_gradle_plugin = "8.12.0"
-kotlin = "2.2.0"
+android_gradle_plugin = "8.12.1"
+# When updateing this, please also update the version in the file ./idea/kotlinc.xml
+kotlin = "2.2.10"
kotlinpoet = "2.2.0"
-ksp = "2.2.0-2.0.2"
+ksp = "2.2.10-2.0.2"
firebaseAppDistribution = "5.1.1"
# AndroidX
-core = "1.16.0"
+core = "1.17.0"
datastore = "1.1.7"
constraintlayout = "2.2.1"
constraintlayout_compose = "1.1.1"
@@ -49,7 +50,7 @@ haze = "1.6.10"
dependencyAnalysis = "2.19.0"
# DI
-dagger = "2.57"
+dagger = "2.57.1"
anvil = "0.4.1"
# Auto service
@@ -171,11 +172,11 @@ serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-jso
kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0"
showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" }
showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" }
-jsoup = "org.jsoup:jsoup:1.21.1"
+jsoup = "org.jsoup:jsoup:1.21.2"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.1.0"
timber = "com.jakewharton.timber:timber:5.0.1"
-matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.8.5"
+matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.8.25"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
@@ -197,8 +198,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.20.2"
-sentry = "io.sentry:sentry-android:8.19.1"
+posthog = "com.posthog:posthog-android:3.20.4"
+sentry = "io.sentry:sentry-android:8.20.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"
@@ -235,7 +236,7 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
anvil = { id = "dev.zacsweers.anvil", version.ref = "anvil" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
-ktlint = "org.jlleitschuh.gradle.ktlint:13.0.0"
+ktlint = "org.jlleitschuh.gradle.ktlint:13.1.0"
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
dependencycheck = "org.owasp.dependencycheck:12.1.3"
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }
@@ -243,6 +244,6 @@ paparazzi = "app.cash.paparazzi:2.0.0-alpha02"
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" }
knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" }
-sonarqube = "org.sonarqube:6.2.0.5505"
+sonarqube = "org.sonarqube:6.3.0.5676"
licensee = "app.cash.licensee:1.13.0"
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
diff --git a/libraries/androidutils/build.gradle.kts b/libraries/androidutils/build.gradle.kts
index 1aa23a09e8..62cef138d2 100644
--- a/libraries/androidutils/build.gradle.kts
+++ b/libraries/androidutils/build.gradle.kts
@@ -31,12 +31,14 @@ dependencies {
implementation(libs.androidx.activity.activity)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.exifinterface)
+ implementation(libs.androidx.datastore.preferences)
api(libs.androidx.browser)
testImplementation(projects.tests.testutils)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.robolectric)
+ testImplementation(libs.androidx.test.ext.junit)
testImplementation(libs.coroutines.core)
testImplementation(libs.coroutines.test)
testImplementation(projects.services.toolbox.test)
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelper.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelper.kt
index a9aa8cf378..3ac32fd3a9 100644
--- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelper.kt
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelper.kt
@@ -27,7 +27,11 @@ class VideoCompressorHelper(
fun getOutputSize(inputSize: Size): Size {
val resultMajor = min(inputSize.major(), maxSize)
val aspectRatio = inputSize.major().toFloat() / inputSize.minor().toFloat()
- return Size(resultMajor, (resultMajor / aspectRatio).roundToInt())
+ return if (inputSize.isLandscape()) {
+ Size(resultMajor, (resultMajor / aspectRatio).roundToInt())
+ } else {
+ Size((resultMajor / aspectRatio).roundToInt(), resultMajor)
+ }
}
/**
@@ -38,9 +42,10 @@ class VideoCompressorHelper(
val pixelsPerFrame = outputSize.width * outputSize.height
// Apparently, 0.1 bits per pixel is a sweet spot for video compression
val bitsPerPixel = 0.1f
- return (pixelsPerFrame * bitsPerPixel * frameRate).toLong() / 1000
+ return (pixelsPerFrame * bitsPerPixel * frameRate).toLong()
}
}
-internal fun Size.major(): Int = if (width > height) width else height
-internal fun Size.minor(): Int = if (width < height) width else height
+private fun Size.isLandscape(): Boolean = width > height
+private fun Size.major(): Int = if (isLandscape()) width else height
+private fun Size.minor(): Int = if (isLandscape()) height else width
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/preferences/DefaultPreferencesCorruptionHandlerFactory.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/preferences/DefaultPreferencesCorruptionHandlerFactory.kt
new file mode 100644
index 0000000000..e6270f4af3
--- /dev/null
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/preferences/DefaultPreferencesCorruptionHandlerFactory.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.androidutils.preferences
+
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.emptyPreferences
+
+object DefaultPreferencesCorruptionHandlerFactory {
+ /**
+ * Creates a [ReplaceFileCorruptionHandler] that will replace the corrupted preferences file with an empty preferences object.
+ */
+ fun replaceWithEmpty(): ReplaceFileCorruptionHandler {
+ return ReplaceFileCorruptionHandler(
+ produceNewData = {
+ // If the preferences file is corrupted, we return an empty preferences object
+ emptyPreferences()
+ },
+ )
+ }
+}
diff --git a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelperTest.kt b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelperTest.kt
new file mode 100644
index 0000000000..96d15d6bfa
--- /dev/null
+++ b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelperTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.androidutils.media
+
+import android.util.Size
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class VideoCompressorHelperTest {
+ @Test
+ fun `test getOutputSize`() {
+ val helper = VideoCompressorHelper(maxSize = 720)
+
+ // Landscape input
+ var inputSize = Size(1920, 1080)
+ var outputSize = helper.getOutputSize(inputSize)
+ assertThat(outputSize).isEqualTo(Size(720, 405))
+
+ // Landscape input small height
+ inputSize = Size(1920, 200)
+ outputSize = helper.getOutputSize(inputSize)
+ assertThat(outputSize).isEqualTo(Size(720, 75))
+
+ // Portrait input
+ inputSize = Size(1080, 1920)
+ outputSize = helper.getOutputSize(inputSize)
+ assertThat(outputSize).isEqualTo(Size(405, 720))
+
+ // Portrait input small width
+ inputSize = Size(200, 1920)
+ outputSize = helper.getOutputSize(inputSize)
+ assertThat(outputSize).isEqualTo(Size(75, 720))
+
+ // Square input
+ inputSize = Size(1000, 1000)
+ outputSize = helper.getOutputSize(inputSize)
+ assertThat(outputSize).isEqualTo(Size(720, 720))
+
+ // Square input same size
+ inputSize = Size(720, 720)
+ outputSize = helper.getOutputSize(inputSize)
+ assertThat(outputSize).isEqualTo(Size(720, 720))
+
+ // Square input no downscaling
+ inputSize = Size(240, 240)
+ outputSize = helper.getOutputSize(inputSize)
+ assertThat(outputSize).isEqualTo(Size(240, 240))
+
+ // Small input landscape (no downscaling)
+ inputSize = Size(640, 480)
+ outputSize = helper.getOutputSize(inputSize)
+ assertThat(outputSize).isEqualTo(Size(640, 480))
+
+ // Small input portrait (no downscaling)
+ inputSize = Size(480, 640)
+ outputSize = helper.getOutputSize(inputSize)
+ assertThat(outputSize).isEqualTo(Size(480, 640))
+ }
+
+ @Test
+ fun `test calculateOptimalBitrate`() {
+ val helper = VideoCompressorHelper(maxSize = 720)
+ val inputSize = Size(1920, 1080)
+ var bitrate = helper.calculateOptimalBitrate(inputSize, frameRate = 30)
+ // Output size will be 720x405, so bitrate = 720*405*0.1*30 = 874800
+ assertThat(bitrate).isEqualTo(874_800L)
+ // Half frame rate, half bitrate
+ bitrate = helper.calculateOptimalBitrate(inputSize, frameRate = 15)
+ assertThat(bitrate).isEqualTo(437_400L)
+ }
+}
diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt
index b7f22cbe23..7085db62f3 100644
--- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt
+++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt
@@ -160,3 +160,17 @@ suspend inline fun runUpdatingState(
}
)
}
+
+inline fun AsyncData.map(
+ transform: (T?) -> R,
+): AsyncData {
+ return when (this) {
+ is AsyncData.Failure -> AsyncData.Failure(
+ error = error,
+ prevData = transform(prevData)
+ )
+ is AsyncData.Loading -> AsyncData.Loading(transform(prevData))
+ is AsyncData.Success -> AsyncData.Success(transform(data))
+ AsyncData.Uninitialized -> AsyncData.Uninitialized
+ }
+}
diff --git a/libraries/dateformatter/impl/src/main/res/values-uz/translations.xml b/libraries/dateformatter/impl/src/main/res/values-uz/translations.xml
new file mode 100644
index 0000000000..204e29be71
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/res/values-uz/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "%1$sda %2$s"
+ "Bu oy"
+
diff --git a/libraries/deeplink/api/build.gradle.kts b/libraries/deeplink/api/build.gradle.kts
new file mode 100644
index 0000000000..01d568df52
--- /dev/null
+++ b/libraries/deeplink/api/build.gradle.kts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022-2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+plugins {
+ id("io.element.android-library")
+}
+
+android {
+ namespace = "io.element.android.libraries.deeplink.api"
+}
+
+dependencies {
+ implementation(projects.libraries.matrix.api)
+}
diff --git a/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeepLinkCreator.kt b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeepLinkCreator.kt
new file mode 100644
index 0000000000..2a29d70bd4
--- /dev/null
+++ b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeepLinkCreator.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.deeplink.api
+
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.api.core.ThreadId
+
+fun interface DeepLinkCreator {
+ fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String
+}
diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkData.kt
similarity index 94%
rename from libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt
rename to libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkData.kt
index 9dc8a90509..d15652b3ee 100644
--- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt
+++ b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkData.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.libraries.deeplink
+package io.element.android.libraries.deeplink.api
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
diff --git a/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkParser.kt b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkParser.kt
new file mode 100644
index 0000000000..d101bbc2ec
--- /dev/null
+++ b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkParser.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.deeplink.api
+
+import android.content.Intent
+
+fun interface DeeplinkParser {
+ fun getFromIntent(intent: Intent): DeeplinkData?
+}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomScope.kt b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/usecase/InviteFriendsUseCase.kt
similarity index 55%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomScope.kt
rename to libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/usecase/InviteFriendsUseCase.kt
index b0a011424a..5c5bcf5043 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomScope.kt
+++ b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/usecase/InviteFriendsUseCase.kt
@@ -5,6 +5,10 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.createroom.impl.di
+package io.element.android.libraries.deeplink.api.usecase
-abstract class CreateRoomScope private constructor()
+import android.app.Activity
+
+interface InviteFriendsUseCase {
+ fun execute(activity: Activity)
+}
diff --git a/libraries/deeplink/build.gradle.kts b/libraries/deeplink/impl/build.gradle.kts
similarity index 90%
rename from libraries/deeplink/build.gradle.kts
rename to libraries/deeplink/impl/build.gradle.kts
index 5e1a06bee0..412c162299 100644
--- a/libraries/deeplink/build.gradle.kts
+++ b/libraries/deeplink/impl/build.gradle.kts
@@ -6,17 +6,19 @@ import extension.setupAnvil
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
+
plugins {
id("io.element.android-library")
}
android {
- namespace = "io.element.android.libraries.deeplink"
+ namespace = "io.element.android.libraries.deeplink.impl"
}
setupAnvil()
dependencies {
+ api(projects.libraries.deeplink.api)
implementation(projects.libraries.di)
implementation(libs.dagger)
implementation(libs.androidx.corektx)
diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/Constants.kt
similarity index 84%
rename from libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt
rename to libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/Constants.kt
index b39a1c97d8..1cd98a70d3 100644
--- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt
+++ b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/Constants.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.libraries.deeplink
+package io.element.android.libraries.deeplink.impl
internal const val SCHEME = "elementx"
internal const val HOST = "open"
diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeepLinkCreator.kt
similarity index 61%
rename from libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt
rename to libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeepLinkCreator.kt
index 25058e1812..371a0d21fb 100644
--- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt
+++ b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeepLinkCreator.kt
@@ -1,19 +1,23 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.libraries.deeplink
+package io.element.android.libraries.deeplink.impl
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.deeplink.api.DeepLinkCreator
+import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
import javax.inject.Inject
-class DeepLinkCreator @Inject constructor() {
- fun room(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String {
+@ContributesBinding(AppScope::class)
+class DefaultDeepLinkCreator @Inject constructor() : DeepLinkCreator {
+ override fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String {
return buildString {
append("$SCHEME://$HOST/")
append(sessionId.value)
diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParser.kt
similarity index 71%
rename from libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt
rename to libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParser.kt
index cdb249b7b9..84b17fc9aa 100644
--- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt
+++ b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParser.kt
@@ -1,21 +1,26 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.libraries.deeplink
+package io.element.android.libraries.deeplink.impl
import android.content.Intent
import android.net.Uri
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.deeplink.api.DeeplinkData
+import io.element.android.libraries.deeplink.api.DeeplinkParser
+import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
import javax.inject.Inject
-class DeeplinkParser @Inject constructor() {
- fun getFromIntent(intent: Intent): DeeplinkData? {
+@ContributesBinding(AppScope::class)
+class DefaultDeeplinkParser @Inject constructor() : DeeplinkParser {
+ override fun getFromIntent(intent: Intent): DeeplinkData? {
return intent
.takeIf { it.action == Intent.ACTION_VIEW }
?.data
diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/usecase/DefaultInviteFriendsUseCase.kt
similarity index 79%
rename from libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt
rename to libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/usecase/DefaultInviteFriendsUseCase.kt
index dc072fc9bc..8c24234cd8 100644
--- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt
+++ b/libraries/deeplink/impl/src/main/kotlin/io/element/android/libraries/deeplink/impl/usecase/DefaultInviteFriendsUseCase.kt
@@ -1,15 +1,18 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.libraries.deeplink.usecase
+package io.element.android.libraries.deeplink.impl.usecase
import android.app.Activity
+import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
import io.element.android.libraries.core.meta.BuildMeta
+import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase
+import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.libraries.ui.strings.CommonStrings
@@ -18,13 +21,14 @@ import timber.log.Timber
import javax.inject.Inject
import io.element.android.libraries.androidutils.R as AndroidUtilsR
-class InviteFriendsUseCase @Inject constructor(
+@ContributesBinding(SessionScope::class)
+class DefaultInviteFriendsUseCase @Inject constructor(
private val stringProvider: StringProvider,
private val matrixClient: MatrixClient,
private val buildMeta: BuildMeta,
private val permalinkBuilder: PermalinkBuilder,
-) {
- fun execute(activity: Activity) {
+) : InviteFriendsUseCase {
+ override fun execute(activity: Activity) {
val permalinkResult = permalinkBuilder.permalinkForUser(matrixClient.sessionId)
permalinkResult.fold(
onSuccess = { permalink ->
diff --git a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt b/libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeepLinkCreatorTest.kt
similarity index 67%
rename from libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt
rename to libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeepLinkCreatorTest.kt
index f6114fabb2..a5c943c525 100644
--- a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt
+++ b/libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeepLinkCreatorTest.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.libraries.deeplink
+package io.element.android.libraries.deeplink.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.A_ROOM_ID
@@ -13,15 +13,15 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID
import org.junit.Test
-class DeepLinkCreatorTest {
+class DefaultDeepLinkCreatorTest {
@Test
- fun room() {
- val sut = DeepLinkCreator()
- assertThat(sut.room(A_SESSION_ID, null, null))
+ fun create() {
+ val sut = DefaultDeepLinkCreator()
+ assertThat(sut.create(A_SESSION_ID, null, null))
.isEqualTo("elementx://open/@alice:server.org")
- assertThat(sut.room(A_SESSION_ID, A_ROOM_ID, null))
+ assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, null))
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain")
- assertThat(sut.room(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
+ assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
}
}
diff --git a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt b/libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParserTest.kt
similarity index 90%
rename from libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt
rename to libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParserTest.kt
index 48e3bab9db..787c721092 100644
--- a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt
+++ b/libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParserTest.kt
@@ -5,11 +5,12 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.libraries.deeplink
+package io.element.android.libraries.deeplink.impl
import android.content.Intent
import androidx.core.net.toUri
import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID
@@ -19,7 +20,7 @@ import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
-class DeeplinkParserTest {
+class DefaultDeeplinkParserTest {
companion object {
const val A_URI =
"elementx://open/@alice:server.org"
@@ -29,10 +30,9 @@ class DeeplinkParserTest {
"elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId"
}
- private val sut = DeeplinkParser()
-
@Test
fun `nominal cases`() {
+ val sut = DefaultDeeplinkParser()
assertThat(sut.getFromIntent(createIntent(A_URI)))
.isEqualTo(DeeplinkData.Root(A_SESSION_ID))
assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM)))
@@ -43,7 +43,7 @@ class DeeplinkParserTest {
@Test
fun `error cases`() {
- val sut = DeeplinkParser()
+ val sut = DefaultDeeplinkParser()
// Bad scheme
assertThat(sut.getFromIntent(createIntent("x://open/@alice:server.org"))).isNull()
// Bad host
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/SelectedIndicatorAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/SelectedIndicatorAtom.kt
new file mode 100644
index 0000000000..4f30ac6d9b
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/SelectedIndicatorAtom.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.designsystem.atomic.atoms
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.selection.toggleable
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.Icon
+
+@Composable
+fun SelectedIndicatorAtom(
+ checked: Boolean,
+ enabled: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ if (checked) {
+ Icon(
+ modifier = modifier.toggleable(
+ value = true,
+ role = Role.Companion.Checkbox,
+ enabled = enabled,
+ onValueChange = {},
+ ),
+ imageVector = CompoundIcons.CheckCircleSolid(),
+ contentDescription = null,
+ tint = if (enabled) {
+ ElementTheme.colors.iconAccentPrimary
+ } else {
+ ElementTheme.colors.iconDisabled
+ },
+ )
+ } else {
+ Box(modifier)
+ }
+}
+
+@Composable
+@PreviewsDayNight
+internal fun SelectedIndicatorAtomPreview() = ElementPreview {
+ Column(
+ modifier = Modifier.padding(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ SelectedIndicatorAtom(
+ modifier = Modifier.size(24.dp),
+ checked = false,
+ enabled = false,
+ )
+ SelectedIndicatorAtom(
+ modifier = Modifier.size(24.dp),
+ checked = true,
+ enabled = false,
+ )
+ SelectedIndicatorAtom(
+ modifier = Modifier.size(24.dp),
+ checked = false,
+ enabled = true,
+ )
+ SelectedIndicatorAtom(
+ modifier = Modifier.size(24.dp),
+ checked = true,
+ enabled = true,
+ )
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/RoomPreviewMembersCountMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/MembersCountMolecule.kt
similarity index 87%
rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/RoomPreviewMembersCountMolecule.kt
rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/MembersCountMolecule.kt
index 07c57d2fac..844376af85 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/RoomPreviewMembersCountMolecule.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/MembersCountMolecule.kt
@@ -25,7 +25,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
-fun RoomPreviewMembersCountMolecule(
+fun MembersCountMolecule(
memberCount: Long,
modifier: Modifier = Modifier,
) {
@@ -51,13 +51,13 @@ fun RoomPreviewMembersCountMolecule(
@PreviewsDayNight
@Composable
-internal fun RoomPreviewMembersCountMoleculePreview() = ElementPreview {
+internal fun MembersCountMoleculePreview() = ElementPreview {
Column(
modifier = Modifier.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
- RoomPreviewMembersCountMolecule(memberCount = 1)
- RoomPreviewMembersCountMolecule(memberCount = 888)
- RoomPreviewMembersCountMolecule(memberCount = 123_456)
+ MembersCountMolecule(memberCount = 1)
+ MembersCountMolecule(memberCount = 888)
+ MembersCountMolecule(memberCount = 123_456)
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt
index 716d4a46ce..810ff7b493 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt
@@ -31,6 +31,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
*
* Ref: https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=133-5427&t=5SHVppfYzjvkEywR-0
* @param modifier Classical modifier.
+ * @param renderBackground whether to render the background image or not.
* @param contentAlignment horizontal alignment of the contents.
* @param footer optional footer.
* @param content main content.
@@ -38,6 +39,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun OnBoardingPage(
modifier: Modifier = Modifier,
+ renderBackground: Boolean = true,
contentAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
footer: @Composable () -> Unit = {},
content: @Composable () -> Unit = {},
@@ -47,13 +49,15 @@ fun OnBoardingPage(
.fillMaxSize()
) {
// BG
- Image(
- modifier = Modifier
- .fillMaxSize(),
- painter = painterResource(id = R.drawable.onboarding_bg),
- contentScale = ContentScale.Crop,
- contentDescription = null,
- )
+ if (renderBackground) {
+ Image(
+ modifier = Modifier
+ .fillMaxSize(),
+ painter = painterResource(id = R.drawable.onboarding_bg),
+ contentScale = ContentScale.Crop,
+ contentDescription = null,
+ )
+ }
Column(
modifier = Modifier
.fillMaxSize()
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt
index 3c209df2cf..7ccebd2082 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt
@@ -16,8 +16,8 @@ open class AvatarDataProvider : PreviewParameterProvider {
.map {
sequenceOf(
anAvatarData(size = it),
- anAvatarData(size = it).copy(name = null),
- anAvatarData(size = it).copy(url = "aUrl"),
+ anAvatarData(size = it, name = null),
+ anAvatarData(size = it, url = "aUrl"),
)
}
.flatten()
@@ -26,10 +26,12 @@ open class AvatarDataProvider : PreviewParameterProvider {
fun anAvatarData(
// Let's the id not start with a 'a'.
id: String = "@id_of_alice:server.org",
- name: String = "Alice",
+ name: String? = "Alice",
+ url: String? = null,
size: AvatarSize = AvatarSize.RoomListItem,
) = AvatarData(
id = id,
name = name,
+ url = url,
size = size,
)
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/AvatarRow.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarRow.kt
similarity index 70%
rename from features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/AvatarRow.kt
rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarRow.kt
index 992098dc91..8c3ae3aac9 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/AvatarRow.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarRow.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.knockrequests.impl.banner
+package io.element.android.libraries.designsystem.components.avatar
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
@@ -23,10 +23,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
-import io.element.android.libraries.designsystem.components.avatar.Avatar
-import io.element.android.libraries.designsystem.components.avatar.AvatarData
-import io.element.android.libraries.designsystem.components.avatar.AvatarSize
-import io.element.android.libraries.designsystem.components.avatar.AvatarType
+import io.element.android.libraries.designsystem.components.avatar.internal.OverlapRatioProvider
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toPx
@@ -41,6 +38,7 @@ import kotlinx.collections.immutable.toImmutableList
* @param modifier Jetpack Compose modifier
* @param overlapRatio the overlap ration. When 0f, avatars will render without overlap, when 1f
* only the first avatar will be visible
+ * @param lastOnTop if true, the last visible avatar will be rendered on top.
*/
@Composable
fun AvatarRow(
@@ -48,6 +46,7 @@ fun AvatarRow(
avatarType: AvatarType,
modifier: Modifier = Modifier,
overlapRatio: Float = 0.5f,
+ lastOnTop: Boolean = false,
) {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
Box(
@@ -57,23 +56,35 @@ fun AvatarRow(
val avatarSize = avatarDataList.firstOrNull()?.size?.dp ?: return
val avatarSizePx = avatarSize.toPx()
avatarDataList
- .reversed()
+ .let {
+ if (lastOnTop) {
+ it
+ } else {
+ it.reversed()
+ }
+ }
.forEachIndexed { index, avatarData ->
+ val startPadding = if (lastOnTop) {
+ avatarSize * (1 - overlapRatio) * index
+ } else {
+ avatarSize * (1 - overlapRatio) * (lastItemIndex - index)
+ }
Avatar(
modifier = Modifier
- .padding(start = avatarSize * (1 - overlapRatio) * (lastItemIndex - index))
+ .padding(start = startPadding)
.graphicsLayer {
compositingStrategy = CompositingStrategy.Offscreen
}
.drawWithContent {
- // Draw content and clear the pixels for the avatar on the left (right in RTL).
+ // Draw content and clear the pixels for the avatar on the left (right in RTL) or when lastOnTop is true on
+ // the right (left in RTL).
drawContent()
- val xOffset = if (isRtl) {
- size.width - avatarSizePx * (overlapRatio - 0.5f)
- } else {
- 0f + avatarSizePx * (overlapRatio - 0.5f)
- }
if (index < lastItemIndex) {
+ val xOffset = if (isRtl == lastOnTop) {
+ avatarSizePx * (overlapRatio - 0.5f)
+ } else {
+ size.width - avatarSizePx * (overlapRatio - 0.5f)
+ }
drawCircle(
color = Color.Black,
center = Offset(
@@ -104,6 +115,17 @@ internal fun AvatarRowPreview(@PreviewParameter(OverlapRatioProvider::class) ove
}
}
+@Composable
+@PreviewsDayNight
+internal fun AvatarRowLastOnTopPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) {
+ ElementPreview {
+ ContentToPreview(
+ overlapRatio = overlapRatio,
+ lastOnTop = true,
+ )
+ }
+}
+
@Composable
@PreviewsDayNight
internal fun AvatarRowRtlPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) {
@@ -117,7 +139,25 @@ internal fun AvatarRowRtlPreview(@PreviewParameter(OverlapRatioProvider::class)
}
@Composable
-private fun ContentToPreview(overlapRatio: Float) {
+@PreviewsDayNight
+internal fun AvatarRowLastOnTopRtlPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) {
+ CompositionLocalProvider(
+ LocalLayoutDirection provides LayoutDirection.Rtl,
+ ) {
+ ElementPreview {
+ ContentToPreview(
+ overlapRatio = overlapRatio,
+ lastOnTop = true,
+ )
+ }
+ }
+}
+
+@Composable
+private fun ContentToPreview(
+ overlapRatio: Float,
+ lastOnTop: Boolean = false,
+) {
AvatarRow(
avatarDataList = listOf("A", "B", "C").map {
AvatarData(
@@ -128,5 +168,6 @@ private fun ContentToPreview(overlapRatio: Float) {
}.toImmutableList(),
avatarType = AvatarType.User,
overlapRatio = overlapRatio,
+ lastOnTop = lastOnTop,
)
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt
index 0f84b82fd1..91d52cfc84 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt
@@ -24,7 +24,7 @@ enum class AvatarSize(val dp: Dp) {
UserHeader(96.dp),
UserListItem(36.dp),
- SelectedUser(56.dp),
+ SelectedUser(52.dp),
SelectedRoom(56.dp),
DmCluster(75.dp),
@@ -63,4 +63,8 @@ enum class AvatarSize(val dp: Dp) {
DmCreationConfirmation(64.dp),
UserVerification(52.dp),
+
+ OrganizationHeader(64.dp),
+ SpaceHeader(64.dp),
+ SpaceMember(24.dp),
}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/OverlapRatioProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/OverlapRatioProvider.kt
similarity index 85%
rename from features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/OverlapRatioProvider.kt
rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/OverlapRatioProvider.kt
index f9ff9ee82e..9a1abcee58 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/OverlapRatioProvider.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/OverlapRatioProvider.kt
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.knockrequests.impl.banner
+package io.element.android.libraries.designsystem.components.avatar.internal
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/StringProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/StringProvider.kt
deleted file mode 100644
index 74c11e58c0..0000000000
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/StringProvider.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.libraries.designsystem.utils
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-
-open class StringProvider(val strings: List) : PreviewParameterProvider {
- override val values: Sequence
- get() = strings.asSequence()
-}
diff --git a/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml b/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml
index 342af5d761..9776510d6f 100644
--- a/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml
+++ b/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml
@@ -19,6 +19,8 @@
"你移除了聊天室头像"
"%1$s 封禁了 %2$s"
"你封禁了 %1$s"
+ "你封禁了%1$s:%2$s"
+ "%1$s封禁了%2$s:%3$s"
"%1$s 创建了聊天室"
"你创建了聊天室"
"%1$s 邀请了 %2$s"
@@ -55,6 +57,8 @@
"你拒绝了邀请"
"%1$s 移除了 %2$s"
"你移除了 %1$s"
+ "您已删除%1$s :%2$s"
+ "%1$s已移除%2$s:%3$s"
"%1$s 向 %2$s 发送了加入聊天室的邀请"
"你邀请 %1$s 加入聊天室"
"%1$s 撤销了 %2$s 加入聊天室的邀请"
diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultBaseRoomLastMessageFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultBaseRoomLastMessageFormatterTest.kt
index 6d0ad1aa49..ee56fcf2c5 100644
--- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultBaseRoomLastMessageFormatterTest.kt
+++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultBaseRoomLastMessageFormatterTest.kt
@@ -14,6 +14,7 @@ import com.google.common.truth.Truth.assertWithMessage
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
@@ -174,7 +175,7 @@ class DefaultBaseRoomLastMessageFormatterTest {
) {
val body = "Shared body"
fun createMessageContent(type: MessageType): MessageContent {
- return MessageContent(body, null, false, false, type)
+ return MessageContent(body, null, false, EventThreadInfo(null, null), type)
}
val sharedContentMessagesTypes = arrayOf(
diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt
index 4ff71608f8..349cd2585f 100644
--- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt
+++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt
@@ -14,6 +14,7 @@ import com.google.common.truth.Truth.assertWithMessage
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
@@ -129,7 +130,7 @@ class DefaultPinnedMessagesBannerFormatterTest {
fun `Message contents`() {
val body = "Shared body"
fun createMessageContent(type: MessageType): MessageContent {
- return MessageContent(body, null, false, false, type)
+ return MessageContent(body, null, false, EventThreadInfo(null, null), type)
}
val sharedContentMessagesTypes = arrayOf(
diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
index 9d34c03a93..f420e6179f 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
@@ -93,4 +93,11 @@ enum class FeatureFlags(
// False so it's displayed in the developer options screen
isFinished = false,
),
+ Threads(
+ key = "feature.thread_timeline",
+ title = "Threads",
+ description = "Renders thread messages as a dedicated timeline. Restarting the app is required for this setting to fully take effect.",
+ defaultValue = { false },
+ isFinished = false,
+ )
}
diff --git a/libraries/featureflag/impl/build.gradle.kts b/libraries/featureflag/impl/build.gradle.kts
index c1da63dba3..bd702fafac 100644
--- a/libraries/featureflag/impl/build.gradle.kts
+++ b/libraries/featureflag/impl/build.gradle.kts
@@ -24,7 +24,9 @@ dependencies {
implementation(libs.androidx.datastore.preferences)
implementation(projects.appconfig)
implementation(projects.libraries.di)
+ implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
+ implementation(projects.libraries.preferences.api)
implementation(libs.coroutines.core)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt
index 0c1f1a45e8..357bf3c548 100644
--- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt
+++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt
@@ -7,30 +7,24 @@
package io.element.android.libraries.featureflag.impl
-import android.content.Context
-import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.preferencesDataStore
import io.element.android.libraries.core.meta.BuildMeta
-import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.featureflag.api.Feature
+import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import javax.inject.Inject
-private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_featureflag")
-
/**
* Note: this will be used only in the nightly and in the debug build.
*/
class PreferencesFeatureFlagProvider @Inject constructor(
- @ApplicationContext context: Context,
private val buildMeta: BuildMeta,
+ preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : MutableFeatureFlagProvider {
- private val store = context.dataStore
+ private val store = preferenceDataStoreFactory.create("elementx_featureflag")
override val priority = MEDIUM_PRIORITY
diff --git a/libraries/featureflag/ui/build.gradle.kts b/libraries/featureflag/ui/build.gradle.kts
index 9d53b5fe07..32fd4f6eab 100644
--- a/libraries/featureflag/ui/build.gradle.kts
+++ b/libraries/featureflag/ui/build.gradle.kts
@@ -1,5 +1,3 @@
-import extension.setupAnvil
-
/*
* Copyright 2023, 2024 New Vector Ltd.
*
@@ -16,8 +14,6 @@ android {
namespace = "io.element.android.libraries.featureflag.ui"
}
-setupAnvil()
-
dependencies {
implementation(projects.libraries.designsystem)
}
diff --git a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt
index 14cc438f41..ca9810c41d 100644
--- a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt
+++ b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt
@@ -21,8 +21,6 @@ import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
@@ -32,7 +30,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
-@SingleIn(AppScope::class)
class FullScreenIntentPermissionsPresenter @Inject constructor(
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
private val externalIntentLauncher: ExternalIntentLauncher,
diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts
index 9cd7e6ac7f..09f22f97cf 100644
--- a/libraries/matrix/api/build.gradle.kts
+++ b/libraries/matrix/api/build.gradle.kts
@@ -1,6 +1,5 @@
import config.BuildTimeConfig
import extension.buildConfigFieldStr
-import extension.setupAnvil
/*
* Copyright 2022-2024 New Vector Ltd.
@@ -42,8 +41,6 @@ android {
}
}
-setupAnvil()
-
dependencies {
implementation(projects.libraries.di)
implementation(libs.dagger)
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
index e8c346cf9e..3c0f95745e 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
@@ -10,7 +10,6 @@ package io.element.android.libraries.matrix.api
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.MatrixPatterns
-import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
@@ -54,6 +53,7 @@ interface MatrixClient {
suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom?
suspend fun getRoom(roomId: RoomId): BaseRoom?
suspend fun findDM(userId: UserId): Result
+ suspend fun getJoinedRoomIds(): Result>
suspend fun ignoreUser(userId: UserId): Result
suspend fun unignoreUser(userId: UserId): Result
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result
@@ -94,7 +94,7 @@ interface MatrixClient {
*/
suspend fun getUserProfile(): Result
suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result
- suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result
+ suspend fun uploadMedia(mimeType: String, data: ByteArray): Result
fun roomMembershipObserver(): RoomMembershipObserver
/**
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt
index 3dc5801d6b..08a9c2eeaa 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt
@@ -20,3 +20,5 @@ value class EventId(val value: String) : Serializable {
override fun toString(): String = value
}
+
+fun EventId.toThreadId(): ThreadId = ThreadId(value)
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CreateTimelineParams.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CreateTimelineParams.kt
index 0bcfb0bf54..7d7b3a379a 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CreateTimelineParams.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CreateTimelineParams.kt
@@ -8,10 +8,12 @@
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.ThreadId
sealed interface CreateTimelineParams {
data class Focused(val focusedEventId: EventId) : CreateTimelineParams
data object MediaOnly : CreateTimelineParams
data class MediaOnlyFocused(val focusedEventId: EventId) : CreateTimelineParams
data object PinnedOnly : CreateTimelineParams
+ data class Threaded(val threadRootEventId: ThreadId) : CreateTimelineParams
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/FilterRoomMembers.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/FilterRoomMembers.kt
new file mode 100644
index 0000000000..a1c4f0bf01
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/FilterRoomMembers.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.api.room
+
+import io.element.android.libraries.core.bool.orFalse
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Method to filter members by userId or displayName.
+ * It does filter through the already known members, it doesn't perform additional requests.
+ */
+suspend fun BaseRoom.filterMembers(query: String, coroutineContext: CoroutineContext): List = withContext(coroutineContext) {
+ val roomMembersState = membersStateFlow.value
+ val activeRoomMembers = roomMembersState.roomMembers()
+ ?.filter { it.membership.isActive() }
+ .orEmpty()
+ val filteredMembers = if (query.isBlank()) {
+ activeRoomMembers
+ } else {
+ activeRoomMembers.filter { member ->
+ member.userId.value.contains(query, ignoreCase = true) ||
+ member.displayName?.contains(query, ignoreCase = true).orFalse()
+ }
+ }
+ filteredMembers
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembersState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembersState.kt
index ab857e5d63..c83b74ac70 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembersState.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembersState.kt
@@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.api.room
import androidx.compose.runtime.Immutable
+import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.collections.immutable.ImmutableList
@Immutable
@@ -34,3 +35,9 @@ fun RoomMembersState.joinedRoomMembers(): List {
fun RoomMembersState.activeRoomMembers(): List {
return roomMembers().orEmpty().filter { it.membership.isActive() }
}
+
+fun RoomMembersState.getDirectRoomMember(roomInfo: RoomInfo, sessionId: SessionId): RoomMember? {
+ return roomMembers()
+ ?.takeIf { roomInfo.isDm }
+ ?.find { it.userId != sessionId && it.membership.isActive() }
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt
index 6fe33d242a..4dfc3ac565 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt
@@ -7,8 +7,10 @@
package io.element.android.libraries.matrix.api.room.join
+import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.RoomId
+@Immutable
sealed interface AllowRule {
data class RoomMembership(val roomId: RoomId) : AllowRule
data class Custom(val json: String) : AllowRule
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt
index f733cb56ac..0eada82e29 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt
@@ -7,12 +7,16 @@
package io.element.android.libraries.matrix.api.room.join
+import androidx.compose.runtime.Immutable
+import kotlinx.collections.immutable.ImmutableList
+
+@Immutable
sealed interface JoinRule {
data object Public : JoinRule
data object Private : JoinRule
data object Knock : JoinRule
data object Invite : JoinRule
- data class Restricted(val rules: List) : JoinRule
- data class KnockRestricted(val rules: List) : JoinRule
+ data class Restricted(val rules: ImmutableList) : JoinRule
+ data class KnockRestricted(val rules: ImmutableList) : JoinRule
data class Custom(val value: String) : JoinRule
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt
index 9bcdafb913..4eceeac4da 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt
@@ -7,9 +7,10 @@
package io.element.android.libraries.matrix.api.timeline
+import android.os.Parcelable
import io.element.android.libraries.matrix.api.core.EventId
-import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
@@ -24,6 +25,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.parcelize.Parcelize
import java.io.File
interface Timeline : AutoCloseable {
@@ -39,13 +41,16 @@ interface Timeline : AutoCloseable {
FORWARDS
}
- enum class Mode {
- LIVE,
- FOCUSED_ON_EVENT,
- PINNED_EVENTS,
- MEDIA,
+ @Parcelize
+ sealed interface Mode : Parcelable {
+ data object Live : Mode
+ data class FocusedOnEvent(val eventId: EventId) : Mode
+ data object PinnedEvents : Mode
+ data object Media : Mode
+ data class Thread(val threadRootId: ThreadId) : Mode
}
+ val mode: Mode
val membershipChangeEventReceived: Flow
suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result
suspend fun paginate(direction: PaginationDirection): Result
@@ -88,7 +93,6 @@ interface Timeline : AutoCloseable {
imageInfo: ImageInfo,
caption: String?,
formattedCaption: String?,
- progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result
@@ -98,7 +102,6 @@ interface Timeline : AutoCloseable {
videoInfo: VideoInfo,
caption: String?,
formattedCaption: String?,
- progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result
@@ -107,7 +110,6 @@ interface Timeline : AutoCloseable {
audioInfo: AudioInfo,
caption: String?,
formattedCaption: String?,
- progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result
@@ -116,7 +118,6 @@ interface Timeline : AutoCloseable {
fileInfo: FileInfo,
caption: String?,
formattedCaption: String?,
- progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result
@@ -145,7 +146,6 @@ interface Timeline : AutoCloseable {
file: File,
audioInfo: AudioInfo,
waveform: List,
- progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/ThreadSummary.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/ThreadSummary.kt
new file mode 100644
index 0000000000..4960330448
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/ThreadSummary.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.api.timeline.item
+
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.matrix.api.core.ThreadId
+import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
+import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
+import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
+
+data class EventThreadInfo(
+ val threadRootId: ThreadId?,
+ val threadSummary: ThreadSummary?,
+)
+
+data class ThreadSummary(
+ val latestEvent: AsyncData,
+ val numberOfReplies: Long,
+)
+
+data class EmbeddedEventInfo(
+ val eventOrTransactionId: EventOrTransactionId,
+ val content: EventContent,
+ val senderId: UserId,
+ val senderProfile: ProfileTimelineDetails,
+ val timestamp: Long,
+)
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt
index bf062b76e0..a6bea83565 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt
@@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.poll.PollAnswer
import io.element.android.libraries.matrix.api.poll.PollKind
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
@@ -23,7 +24,7 @@ data class MessageContent(
val body: String,
val inReplyTo: InReplyTo?,
val isEdited: Boolean,
- val isThreaded: Boolean,
+ val threadInfo: EventThreadInfo,
val type: MessageType
) : EventContent
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt
index 0438df4032..fc08c1cee1 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt
@@ -11,6 +11,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.SendHandle
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import kotlinx.collections.immutable.ImmutableList
@@ -37,9 +38,7 @@ data class EventTimelineItem(
return (content as? MessageContent)?.inReplyTo
}
- fun isThreaded(): Boolean {
- return (content as? MessageContent)?.isThreaded ?: false
- }
+ fun threadInfo(): EventThreadInfo? = (content as? MessageContent)?.threadInfo
fun hasNotLoadedInReplyTo(): Boolean {
val details = inReplyTo()
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt
index d43ec0b8ce..f28ef46c66 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt
@@ -14,7 +14,14 @@ import io.element.android.libraries.matrix.api.core.UserId
@Immutable
sealed interface LocalEventSendState {
- data object Sending : LocalEventSendState
+ sealed interface Sending : LocalEventSendState {
+ data object Event : Sending
+ data class MediaWithProgress(
+ val index: Long,
+ val progress: Long,
+ val total: Long
+ ) : Sending
+ }
sealed interface Failed : LocalEventSendState {
data class Unknown(val error: String) : Failed
data object SendingFromUnverifiedDevice : Failed
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 bb5ff45e15..85c1dde7e2 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
@@ -14,9 +14,9 @@ import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
+import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.DeviceId
-import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
@@ -48,7 +48,6 @@ 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.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService
import io.element.android.libraries.matrix.impl.exception.mapClientException
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
@@ -108,6 +107,7 @@ import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientException
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
+import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
import org.matrix.rustcomponents.sdk.PowerLevels
import org.matrix.rustcomponents.sdk.RoomInfoListener
@@ -135,6 +135,7 @@ class RustMatrixClient(
baseCacheDirectory: File,
clock: SystemClock,
timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
+ private val featureFlagService: FeatureFlagService,
) : MatrixClient {
override val sessionId: UserId = UserId(innerClient.userId())
override val deviceId: DeviceId = DeviceId(innerClient.deviceId())
@@ -205,6 +206,7 @@ class RustMatrixClient(
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
roomMembershipObserver = roomMembershipObserver,
roomInfoMapper = roomInfoMapper,
+ featureFlagService = featureFlagService,
)
override val mediaLoader: MatrixMediaLoader = RustMediaLoader(
@@ -276,6 +278,7 @@ class RustMatrixClient(
}
override suspend fun getRoom(roomId: RoomId): BaseRoom? = withContext(sessionDispatcher) {
+ innerClient.rooms()
roomFactory.getBaseRoom(roomId)
}
@@ -310,6 +313,15 @@ class RustMatrixClient(
}
}
+ override suspend fun getJoinedRoomIds(): Result> = withContext(sessionDispatcher) {
+ runCatchingExceptions {
+ innerClient.rooms()
+ .filter { it.membership() == Membership.JOINED }
+ .map { RoomId(it.id()) }
+ .toSet()
+ }
+ }
+
override suspend fun ignoreUser(userId: UserId): Result = withContext(sessionDispatcher) {
runCatchingExceptions {
innerClient.ignoreUser(userId.value)
@@ -627,9 +639,9 @@ class RustMatrixClient(
}
}
- override suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result = withContext(sessionDispatcher) {
+ override suspend fun uploadMedia(mimeType: String, data: ByteArray): Result = withContext(sessionDispatcher) {
runCatchingExceptions {
- innerClient.uploadMedia(mimeType, data, progressCallback?.toProgressWatcher())
+ innerClient.uploadMedia(mimeType, data, progressWatcher = null)
}
}
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 2a4abe6dc2..08ef1a9f4b 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
@@ -93,6 +93,7 @@ class RustMatrixClientFactory @Inject constructor(
baseCacheDirectory = cacheDirectory,
clock = clock,
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
+ featureFlagService = featureFlagService,
).also {
Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
}
@@ -131,6 +132,7 @@ class RustMatrixClientFactory @Inject constructor(
)
)
.enableShareHistoryOnInvite(featureFlagService.isFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite))
+ .threadsEnabled(featureFlagService.isFeatureEnabled(FeatureFlags.Threads), threadSubscriptions = false)
.run {
// Apply sliding sync version settings
when (slidingSyncType) {
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt
index 5edff19d84..baa9f85906 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt
@@ -11,6 +11,8 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
+import io.element.android.libraries.featureflag.api.FeatureFlagService
+import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias
@@ -83,6 +85,7 @@ class JoinedRustRoom(
private val coroutineDispatchers: CoroutineDispatchers,
private val systemClock: SystemClock,
private val roomContentForwarder: RoomContentForwarder,
+ private val featureFlagService: FeatureFlagService,
) : JoinedRoom, BaseRoom by baseRoom {
// Create a dispatcher for all room methods...
private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32)
@@ -132,7 +135,7 @@ class JoinedRustRoom(
override val roomNotificationSettingsStateFlow = MutableStateFlow(RoomNotificationSettingsState.Unknown)
- override val liveTimeline = liveInnerTimeline.map(mode = Timeline.Mode.LIVE) {
+ override val liveTimeline = liveInnerTimeline.map(mode = Timeline.Mode.Live) {
syncUpdateFlow.value = systemClock.epochMillis()
}
@@ -153,22 +156,27 @@ class JoinedRustRoom(
override suspend fun createTimeline(
createTimelineParams: CreateTimelineParams,
): Result = withContext(roomDispatcher) {
+ val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
val focus = when (createTimelineParams) {
is CreateTimelineParams.PinnedOnly -> TimelineFocus.PinnedEvents(
maxEventsToLoad = 100u,
maxConcurrentRequests = 10u,
)
- is CreateTimelineParams.MediaOnly -> TimelineFocus.Live(hideThreadedEvents = false)
+ is CreateTimelineParams.MediaOnly -> TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents)
is CreateTimelineParams.Focused -> TimelineFocus.Event(
eventId = createTimelineParams.focusedEventId.value,
numContextEvents = 50u,
- hideThreadedEvents = false,
+ hideThreadedEvents = hideThreadedEvents,
)
is CreateTimelineParams.MediaOnlyFocused -> TimelineFocus.Event(
eventId = createTimelineParams.focusedEventId.value,
numContextEvents = 50u,
+ // Never hide threaded events in media focused timeline
hideThreadedEvents = false,
)
+ is CreateTimelineParams.Threaded -> TimelineFocus.Thread(
+ rootEventId = createTimelineParams.threadRootEventId.value,
+ )
}
val filter = when (createTimelineParams) {
@@ -182,7 +190,8 @@ class JoinedRustRoom(
)
)
is CreateTimelineParams.Focused,
- CreateTimelineParams.PinnedOnly -> TimelineFilter.All
+ CreateTimelineParams.PinnedOnly,
+ is CreateTimelineParams.Threaded -> TimelineFilter.All
}
val internalIdPrefix = when (createTimelineParams) {
@@ -190,6 +199,7 @@ class JoinedRustRoom(
is CreateTimelineParams.Focused -> "focus_${createTimelineParams.focusedEventId}"
is CreateTimelineParams.MediaOnly -> "MediaGallery_"
is CreateTimelineParams.MediaOnlyFocused -> "MediaGallery_${createTimelineParams.focusedEventId}"
+ is CreateTimelineParams.Threaded -> "Thread_${createTimelineParams.threadRootEventId}"
}
// Note that for TimelineFilter.MediaOnlyFocused, the date separator will be filtered out,
@@ -198,7 +208,8 @@ class JoinedRustRoom(
is CreateTimelineParams.MediaOnly,
is CreateTimelineParams.MediaOnlyFocused -> DateDividerMode.MONTHLY
is CreateTimelineParams.Focused,
- CreateTimelineParams.PinnedOnly -> DateDividerMode.DAILY
+ CreateTimelineParams.PinnedOnly,
+ is CreateTimelineParams.Threaded -> DateDividerMode.DAILY
}
// Track read receipts only for focused timeline for performance optimization
@@ -216,17 +227,19 @@ class JoinedRustRoom(
)
).let { innerTimeline ->
val mode = when (createTimelineParams) {
- is CreateTimelineParams.Focused -> Timeline.Mode.FOCUSED_ON_EVENT
- is CreateTimelineParams.MediaOnly -> Timeline.Mode.MEDIA
- is CreateTimelineParams.MediaOnlyFocused -> Timeline.Mode.FOCUSED_ON_EVENT
- CreateTimelineParams.PinnedOnly -> Timeline.Mode.PINNED_EVENTS
+ is CreateTimelineParams.Focused -> Timeline.Mode.FocusedOnEvent(createTimelineParams.focusedEventId)
+ is CreateTimelineParams.MediaOnly -> Timeline.Mode.Media
+ is CreateTimelineParams.MediaOnlyFocused -> Timeline.Mode.FocusedOnEvent(createTimelineParams.focusedEventId)
+ CreateTimelineParams.PinnedOnly -> Timeline.Mode.PinnedEvents
+ is CreateTimelineParams.Threaded -> Timeline.Mode.Thread(createTimelineParams.threadRootEventId)
}
innerTimeline.map(mode = mode)
}
}.mapFailure {
when (createTimelineParams) {
is CreateTimelineParams.Focused,
- is CreateTimelineParams.MediaOnlyFocused -> it.toFocusEventException()
+ is CreateTimelineParams.MediaOnlyFocused,
+ is CreateTimelineParams.Threaded -> it.toFocusEventException()
CreateTimelineParams.MediaOnly,
CreateTimelineParams.PinnedOnly -> it
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt
index e53ac5c478..ee9e7bfe52 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt
@@ -9,6 +9,8 @@ package io.element.android.libraries.matrix.impl.room
import io.element.android.appconfig.TimelineConfig
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.featureflag.api.FeatureFlagService
+import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
@@ -48,6 +50,7 @@ class RustRoomFactory(
private val innerRoomListService: InnerRoomListService,
private val roomSyncSubscriber: RoomSyncSubscriber,
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
+ private val featureFlagService: FeatureFlagService,
private val roomMembershipObserver: RoomMembershipObserver,
private val roomInfoMapper: RoomInfoMapper,
) {
@@ -105,10 +108,11 @@ class RustRoomFactory(
val sdkRoom = awaitRoomInRoomList(roomId) ?: return@withContext null
if (sdkRoom.membership() == Membership.JOINED) {
+ val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
// Init the live timeline in the SDK from the Room
val timeline = sdkRoom.timelineWithConfiguration(
TimelineConfiguration(
- focus = TimelineFocus.Live(hideThreadedEvents = false),
+ focus = TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents),
filter = eventFilters?.let(TimelineFilter::EventTypeFilter) ?: TimelineFilter.All,
internalIdPrefix = "live",
dateDividerMode = DateDividerMode.DAILY,
@@ -125,6 +129,7 @@ class RustRoomFactory(
liveInnerTimeline = timeline,
coroutineDispatchers = dispatchers,
systemClock = systemClock,
+ featureFlagService = featureFlagService,
)
)
} else {
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt
index bc10a369a4..51422787e2 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt
@@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.impl.room.join
import io.element.android.libraries.matrix.api.room.join.JoinRule
+import kotlinx.collections.immutable.toPersistentList
import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule
fun RustJoinRule.map(): JoinRule {
@@ -16,9 +17,9 @@ fun RustJoinRule.map(): JoinRule {
RustJoinRule.Private -> JoinRule.Private
RustJoinRule.Knock -> JoinRule.Knock
RustJoinRule.Invite -> JoinRule.Invite
- is RustJoinRule.Restricted -> JoinRule.Restricted(rules.map { it.map() })
+ is RustJoinRule.Restricted -> JoinRule.Restricted(rules.map { it.map() }.toPersistentList())
is RustJoinRule.Custom -> JoinRule.Custom(repr)
- is RustJoinRule.KnockRestricted -> JoinRule.KnockRestricted(rules.map { it.map() })
+ is RustJoinRule.KnockRestricted -> JoinRule.KnockRestricted(rules.map { it.map() }.toPersistentList())
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt
index b1540d353d..97d9ef68c1 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt
@@ -7,9 +7,9 @@
package io.element.android.libraries.matrix.impl.timeline
+import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.EventId
-import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
@@ -27,7 +27,6 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.TimelineException
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
-import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.media.toMSC3246range
@@ -81,7 +80,7 @@ private const val PAGINATION_SIZE = 50
class RustTimeline(
private val inner: InnerTimeline,
- mode: Timeline.Mode,
+ override val mode: Timeline.Mode,
systemClock: SystemClock,
private val joinedRoom: JoinedRoom,
private val coroutineScope: CoroutineScope,
@@ -120,19 +119,20 @@ class RustTimeline(
private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode)
override val backwardPaginationStatus = MutableStateFlow(
- Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PINNED_EVENTS)
+ Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PinnedEvents)
)
override val forwardPaginationStatus = MutableStateFlow(
- Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode == Timeline.Mode.FOCUSED_ON_EVENT)
+ Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode !is Timeline.Mode.FocusedOnEvent)
)
init {
- if (mode != Timeline.Mode.PINNED_EVENTS) {
- coroutineScope.fetchMembers()
+ when (mode) {
+ is Timeline.Mode.Live, is Timeline.Mode.FocusedOnEvent -> coroutineScope.fetchMembers()
+ else -> Unit
}
- if (mode == Timeline.Mode.LIVE) {
+ if (mode == Timeline.Mode.Live) {
// When timeline is live, we need to listen to the back pagination status as
// sdk can automatically paginate backwards.
coroutineScope.registerBackPaginationStatusListener()
@@ -221,6 +221,7 @@ class RustTimeline(
items = items,
hasMoreToLoadBackward = backwardPaginationStatus.hasMoreToLoad,
hasMoreToLoadForward = forwardPaginationStatus.hasMoreToLoad,
+ timelineMode = mode,
)
}
.let { items ->
@@ -336,9 +337,9 @@ class RustTimeline(
imageInfo: ImageInfo,
caption: String?,
formattedCaption: String?,
- progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result {
+ Timber.d("Sending image ${file.path.hash()}")
return sendAttachment(listOfNotNull(file, thumbnailFile)) {
inner.sendImage(
params = UploadParameters(
@@ -347,13 +348,11 @@ class RustTimeline(
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
- useSendQueue = true,
mentions = null,
inReplyTo = inReplyToEventId?.value,
),
- thumbnailPath = thumbnailFile?.path,
+ thumbnailSource = thumbnailFile?.path?.let(UploadSource::File),
imageInfo = imageInfo.map(),
- progressWatcher = progressCallback?.toProgressWatcher()
)
}
}
@@ -364,9 +363,9 @@ class RustTimeline(
videoInfo: VideoInfo,
caption: String?,
formattedCaption: String?,
- progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result {
+ Timber.d("Sending video ${file.path.hash()}")
return sendAttachment(listOfNotNull(file, thumbnailFile)) {
inner.sendVideo(
params = UploadParameters(
@@ -375,13 +374,11 @@ class RustTimeline(
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
- useSendQueue = true,
mentions = null,
inReplyTo = inReplyToEventId?.value,
),
- thumbnailPath = thumbnailFile?.path,
+ thumbnailSource = thumbnailFile?.path?.let(UploadSource::File),
videoInfo = videoInfo.map(),
- progressWatcher = progressCallback?.toProgressWatcher()
)
}
}
@@ -391,9 +388,9 @@ class RustTimeline(
audioInfo: AudioInfo,
caption: String?,
formattedCaption: String?,
- progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result {
+ Timber.d("Sending audio ${file.path.hash()}")
return sendAttachment(listOf(file)) {
inner.sendAudio(
params = UploadParameters(
@@ -402,12 +399,10 @@ class RustTimeline(
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
- useSendQueue = true,
mentions = null,
inReplyTo = inReplyToEventId?.value,
),
audioInfo = audioInfo.map(),
- progressWatcher = progressCallback?.toProgressWatcher()
)
}
}
@@ -417,9 +412,9 @@ class RustTimeline(
fileInfo: FileInfo,
caption: String?,
formattedCaption: String?,
- progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result {
+ Timber.d("Sending file ${file.path.hash()}")
return sendAttachment(listOf(file)) {
inner.sendFile(
params = UploadParameters(
@@ -428,12 +423,10 @@ class RustTimeline(
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
- useSendQueue = true,
mentions = null,
inReplyTo = inReplyToEventId?.value,
),
fileInfo = fileInfo.map(),
- progressWatcher = progressCallback?.toProgressWatcher(),
)
}
}
@@ -479,7 +472,6 @@ class RustTimeline(
file: File,
audioInfo: AudioInfo,
waveform: List,
- progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result {
return sendAttachment(listOf(file)) {
@@ -489,13 +481,11 @@ class RustTimeline(
// Maybe allow a caption in the future?
caption = null,
formattedCaption = null,
- useSendQueue = true,
mentions = null,
inReplyTo = inReplyToEventId?.value,
),
audioInfo = audioInfo.map(),
waveform = waveform.toMSC3246range(),
- progressWatcher = progressCallback?.toProgressWatcher(),
)
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt
index d806c9492f..aa20ffd267 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt
@@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.impl.timeline.item.event
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
@@ -37,14 +38,14 @@ private const val MSG_TYPE_GALLERY_UNSTABLE = "dm.filament.gallery"
class EventMessageMapper {
private val inReplyToMapper by lazy { InReplyToMapper(TimelineEventContentMapper()) }
- fun map(message: MsgLikeKind.Message, inReplyTo: InReplyToDetails?, isThreaded: Boolean): MessageContent = message.use {
+ fun map(message: MsgLikeKind.Message, inReplyTo: InReplyToDetails?, threadInfo: EventThreadInfo): MessageContent = message.use {
val type = it.content.msgType.use(this::mapMessageType)
val inReplyToEvent: InReplyTo? = inReplyTo?.use(inReplyToMapper::map)
MessageContent(
body = it.content.body,
inReplyTo = inReplyToEvent,
isEdited = it.content.isEdited,
- isThreaded = isThreaded,
+ threadInfo = threadInfo,
type = type
)
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventOrTransactionIdExtension.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventOrTransactionIdExtension.kt
new file mode 100644
index 0000000000..d247bb72d0
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventOrTransactionIdExtension.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.impl.timeline.item.event
+
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.TransactionId
+import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
+import org.matrix.rustcomponents.sdk.EventOrTransactionId as RustEventOrTransactionId
+
+fun RustEventOrTransactionId.map(): EventOrTransactionId = when (this) {
+ is RustEventOrTransactionId.EventId -> EventOrTransactionId.Event(EventId(eventId))
+ is RustEventOrTransactionId.TransactionId -> EventOrTransactionId.Transaction(TransactionId(transactionId))
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt
index 7cea0e29f4..d923cd420c 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt
@@ -79,7 +79,18 @@ fun RustProfileDetails.map(): ProfileTimelineDetails {
fun RustEventSendState?.map(): LocalEventSendState? {
return when (this) {
null -> null
- RustEventSendState.NotSentYet -> LocalEventSendState.Sending
+ is RustEventSendState.NotSentYet -> {
+ val mediaUploadProgress = this.progress
+ if (mediaUploadProgress != null) {
+ LocalEventSendState.Sending.MediaWithProgress(
+ index = mediaUploadProgress.index.toLong(),
+ progress = mediaUploadProgress.progress.current.toLong(),
+ total = mediaUploadProgress.progress.total.toLong(),
+ )
+ } else {
+ LocalEventSendState.Sending.Event
+ }
+ }
is RustEventSendState.SendingFailed -> {
when (val queueWedgeError = error) {
QueueWedgeError.CrossVerificationRequired -> {
@@ -98,7 +109,7 @@ fun RustEventSendState?.map(): LocalEventSendState? {
}
is QueueWedgeError.GenericApiError -> {
if (isRecoverable) {
- LocalEventSendState.Sending
+ LocalEventSendState.Sending.Event
} else {
LocalEventSendState.Failed.Unknown(queueWedgeError.msg)
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt
index a62f18ddd1..eeb063b28a 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt
@@ -7,7 +7,12 @@
package io.element.android.libraries.matrix.impl.timeline.item.event
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.timeline.item.EmbeddedEventInfo
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
+import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary
import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
@@ -27,6 +32,7 @@ import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.poll.map
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
+import org.matrix.rustcomponents.sdk.EmbeddedEventDetails
import org.matrix.rustcomponents.sdk.MsgLikeKind
import org.matrix.rustcomponents.sdk.TimelineItemContent
import org.matrix.rustcomponents.sdk.use
@@ -59,8 +65,35 @@ class TimelineEventContentMapper(
when (val kind = it.content.kind) {
is MsgLikeKind.Message -> {
val inReplyTo = it.content.inReplyTo
- val isThreaded = it.content.threadRoot != null
- eventMessageMapper.map(kind, inReplyTo, isThreaded)
+ val threadSummary = it.content.threadSummary?.use { summary ->
+ val numberOfReplies = summary.numReplies().toLong()
+ val latestEvent = summary.latestEvent()
+ val details = when (latestEvent) {
+ is EmbeddedEventDetails.Unavailable -> AsyncData.Uninitialized
+ is EmbeddedEventDetails.Pending -> AsyncData.Loading()
+ is EmbeddedEventDetails.Error -> AsyncData.Failure(IllegalStateException(latestEvent.message))
+ is EmbeddedEventDetails.Ready -> {
+ AsyncData.Success(
+ EmbeddedEventInfo(
+ eventOrTransactionId = latestEvent.eventOrTransactionId.map(),
+ content = map(latestEvent.content),
+ senderId = UserId(latestEvent.sender),
+ senderProfile = latestEvent.senderProfile.map(),
+ timestamp = latestEvent.timestamp.toLong()
+ )
+ )
+ }
+ }
+ ThreadSummary(
+ latestEvent = details,
+ numberOfReplies = numberOfReplies,
+ )
+ }
+ val threadInfo = EventThreadInfo(
+ threadRootId = it.content.threadRoot?.let(::ThreadId),
+ threadSummary = threadSummary,
+ )
+ eventMessageMapper.map(kind, inReplyTo, threadInfo)
}
is MsgLikeKind.Redacted -> {
RedactedContent
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt
index 9be7dddddb..c3e8237b72 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt
@@ -24,7 +24,7 @@ class LastForwardIndicatorsPostProcessor(
items: List,
): List