diff --git a/CHANGES.md b/CHANGES.md
index b06cdd6603..f4455d4fc1 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,57 @@
+Changes in Element X v25.06.0
+=============================
+
+Rust SDK: https://github.com/matrix-org/matrix-rust-sdk/releases/tag/matrix-sdk-ffi%2F20250603
+
+## What's Changed
+### ✨ Features
+* Add support for login link by @bmarty in https://github.com/element-hq/element-x-android/pull/4752
+### 🙌 Improvements
+* On boarding flow: add a screen to select account provider among a fixed list by @bmarty in https://github.com/element-hq/element-x-android/pull/4769
+* Change : RoomMember moderation by @ganfra in https://github.com/element-hq/element-x-android/pull/4779
+### 🐛 Bugfixes
+* Fix left room membership change by @ganfra in https://github.com/element-hq/element-x-android/pull/4765
+* fix: exclude more domains from being backed up by the system by @lucasmz-dev in https://github.com/element-hq/element-x-android/pull/4773
+* Make sure HeaderFooterPage contents can be scrolled by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4704
+* Fix mobile link by @bmarty in https://github.com/element-hq/element-x-android/pull/4805
+### 🗣 Translations
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4775
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4804
+### 🧱 Build
+* Maestro: fix MAS and EC breaking the tests by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4762
+* Update Gradle Wrapper from 8.14 to 8.14.1 by @ElementBot in https://github.com/element-hq/element-x-android/pull/4766
+* Stronger lambda error by @bmarty in https://github.com/element-hq/element-x-android/pull/4771
+* Use Localazy's `langAliases` for Indonesian language by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4801
+### Dependency upgrades
+* fix(deps): update datastore to v1.1.7 by @renovate in https://github.com/element-hq/element-x-android/pull/4754
+* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.8.8 by @renovate in https://github.com/element-hq/element-x-android/pull/4721
+* chore(deps): update plugin ktlint to v12.3.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4767
+* fix(deps): update dependency com.google.firebase:firebase-bom to v33.14.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4755
+* Update UnifiedPush library by @bmarty in https://github.com/element-hq/element-x-android/pull/4358
+* fix(deps): update sqldelight to v2.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4735
+* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.5.26 by @renovate in https://github.com/element-hq/element-x-android/pull/4781
+* fix(deps): update dependency com.posthog:posthog-android to v3.15.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4787
+* fix(deps): update dependency com.posthog:posthog-android to v3.16.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4789
+* fix(deps): update dependency io.element.android:element-call-embedded to v0.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4743
+* fix(deps): update dependencyanalysis to v2.18.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4796
+* fix(deps): update android.gradle.plugin to v8.10.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4795
+* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.5.29 by @renovate in https://github.com/element-hq/element-x-android/pull/4799
+* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.6.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4810
+### Others
+* fix(deps): update media3 to v1.7.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4733
+* fix: Ignore global proxy settings if system thinks there's none by @ShadowRZ in https://github.com/element-hq/element-x-android/pull/4744
+* Add `ActiveRoomHolder` to manage the active room for a session by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4758
+* Notification events resolving and rendering in batches by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4722
+* Hide Element Call entry point if Element Call service is not available. by @bmarty in https://github.com/element-hq/element-x-android/pull/4783
+* Fix dependencies on test by @bmarty in https://github.com/element-hq/element-x-android/pull/4790
+* Update _developer_onboarding.md by @lex-neufeld in https://github.com/element-hq/element-x-android/pull/4570
+
+## New Contributors
+* @lucasmz-dev made their first contribution in https://github.com/element-hq/element-x-android/pull/4773
+* @lex-neufeld made their first contribution in https://github.com/element-hq/element-x-android/pull/4570
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.05.4...v25.06.0
+
Changes in Element X v25.05.4
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6293a9fe57..adc2b178f8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -25,6 +25,10 @@
android:theme="@style/Theme.ElementX"
tools:targetApi="33">
+
+
+
+
+
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 abcdca12cc..9a07c04f57 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
@@ -65,6 +65,7 @@ import io.element.android.libraries.architecture.waitForNavTargetAttached
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
@@ -104,7 +105,8 @@ class LoggedInFlowNode @AssistedInject constructor(
private val secureBackupEntryPoint: SecureBackupEntryPoint,
private val userProfileEntryPoint: UserProfileEntryPoint,
private val ftueEntryPoint: FtueEntryPoint,
- private val coroutineScope: CoroutineScope,
+ @SessionCoroutineScope
+ private val sessionCoroutineScope: CoroutineScope,
private val ftueService: FtueService,
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
private val shareEntryPoint: ShareEntryPoint,
@@ -175,7 +177,7 @@ class LoggedInFlowNode @AssistedInject constructor(
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
// TODO We do not support Space yet, so directly navigate to main space
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
- loggedInFlowProcessor.observeEvents(coroutineScope)
+ loggedInFlowProcessor.observeEvents(sessionCoroutineScope)
matrixClient.sessionVerificationService().setListener(verificationListener)
ftueService.state
@@ -313,7 +315,7 @@ class LoggedInFlowNode @AssistedInject constructor(
}
override fun onForwardedToSingleRoom(roomId: RoomId) {
- coroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias(), clearBackstack = false) }
+ sessionCoroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias(), clearBackstack = false) }
}
override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt
index 2a7403862e..7ef73986ac 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt
@@ -21,6 +21,7 @@ import im.vector.app.features.analytics.plan.CryptoSessionStateChange
import im.vector.app.features.analytics.plan.UserProperties
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient
@@ -125,7 +126,7 @@ class LoggedInPresenter @Inject constructor(
}
// Force the user to log out if they were using the proxy sliding sync as it's no longer supported by the SDK
- private suspend fun MatrixClient.needsForcedNativeSlidingSyncMigration(): Result = runCatching {
+ private suspend fun MatrixClient.needsForcedNativeSlidingSyncMigration(): Result = runCatchingExceptions {
val currentSlidingSyncVersion = currentSlidingSyncVersion().getOrThrow()
currentSlidingSyncVersion == SlidingSyncVersion.Proxy
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt
index 0dca1d6221..103e97b258 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt
@@ -42,8 +42,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
-import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
-import io.element.android.libraries.matrix.api.getRoomInfoFlow
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
@@ -124,7 +122,7 @@ class RoomFlowNode @AssistedInject constructor(
}
private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List) {
- val roomInfoFlow = client.getRoomInfoFlow(roomIdOrAlias = roomId.toRoomIdOrAlias())
+ val roomInfoFlow = client.getRoomInfoFlow(roomId)
val isSpaceFlow = roomInfoFlow.map { it.getOrNull()?.isSpace.orFalse() }.distinctUntilChanged()
val currentMembershipFlow = roomInfoFlow.map { it.getOrNull()?.currentUserMembership }.distinctUntilChanged()
combine(currentMembershipFlow, isSpaceFlow) { membership, isSpace ->
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt
index c6b42e1d11..3e5947aada 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt
@@ -30,6 +30,7 @@ import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
@@ -50,7 +51,8 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
private val messagesEntryPoint: MessagesEntryPoint,
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
private val appNavigationStateService: AppNavigationStateService,
- private val appCoroutineScope: CoroutineScope,
+ @SessionCoroutineScope
+ private val sessionCoroutineScope: CoroutineScope,
private val matrixClient: MatrixClient,
private val activeRoomsHolder: ActiveRoomsHolder,
roomComponentFactory: RoomComponentFactory,
@@ -92,7 +94,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
trackVisitedRoom()
},
onResume = {
- appCoroutineScope.launch {
+ sessionCoroutineScope.launch {
inputs.room.subscribeToSync()
}
},
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt
index 15175303dd..bbaa196520 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt
@@ -109,7 +109,7 @@ class JoinedRoomLoadedFlowNodeTest {
messagesEntryPoint = messagesEntryPoint,
roomDetailsEntryPoint = roomDetailsEntryPoint,
appNavigationStateService = FakeAppNavigationStateService(),
- appCoroutineScope = this,
+ sessionCoroutineScope = this,
roomComponentFactory = FakeRoomComponentFactory(),
matrixClient = FakeMatrixClient(),
activeRoomsHolder = activeRoomsHolder,
diff --git a/build.gradle.kts b/build.gradle.kts
index 56fcb0ec7b..317a6ab5bd 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -50,6 +50,7 @@ allprojects {
}
dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:0.4.22")
+ detektPlugins(project(":tests:detekt-rules"))
}
tasks.withType().configureEach {
diff --git a/fastlane/metadata/android/en-US/changelogs/202506010.txt b/fastlane/metadata/android/en-US/changelogs/202506010.txt
new file mode 100644
index 0000000000..a51bde477f
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202506010.txt
@@ -0,0 +1,2 @@
+Main changes in this version: fix audio devices and volume selection in Element Call, improves moderation features.
+Full changelog: https://github.com/element-hq/element-x-android/releases
diff --git a/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt b/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt
index 16a8a151de..86e6432cc5 100644
--- a/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt
+++ b/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt
@@ -10,8 +10,10 @@ package io.element.android.features.cachecleaner.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.cachecleaner.api.CacheCleaner
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.CacheDirectory
+import io.element.android.libraries.di.annotations.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -23,7 +25,8 @@ import javax.inject.Inject
*/
@ContributesBinding(AppScope::class)
class DefaultCacheCleaner @Inject constructor(
- private val scope: CoroutineScope,
+ @AppCoroutineScope
+ private val coroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
@CacheDirectory private val cacheDir: File,
) : CacheCleaner {
@@ -32,8 +35,8 @@ class DefaultCacheCleaner @Inject constructor(
}
override fun clearCache() {
- scope.launch(dispatchers.io) {
- runCatching {
+ coroutineScope.launch(dispatchers.io) {
+ runCatchingExceptions {
SUBDIRS_TO_CLEANUP.forEach {
File(cacheDir.path, it).apply {
if (exists()) {
diff --git a/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt b/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt
index 884d29ac88..997c0fe703 100644
--- a/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt
+++ b/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt
@@ -55,7 +55,7 @@ class DefaultCacheCleanerTest {
}
private fun TestScope.aCacheCleaner() = DefaultCacheCleaner(
- scope = this,
+ coroutineScope = this,
dispatchers = this.testCoroutineDispatchers(true),
cacheDir = temporaryFolder.root,
)
diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt
index 0456751ae8..2c279b7725 100644
--- a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt
+++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt
@@ -15,11 +15,19 @@ import kotlinx.parcelize.Parcelize
sealed interface CallType : NodeInputs, Parcelable {
@Parcelize
- data class ExternalUrl(val url: String) : CallType
+ data class ExternalUrl(val url: String) : CallType {
+ override fun toString(): String {
+ return "ExternalUrl"
+ }
+ }
@Parcelize
data class RoomCall(
val sessionId: SessionId,
val roomId: RoomId,
- ) : CallType
+ ) : CallType {
+ override fun toString(): String {
+ return "RoomCall(sessionId=$sessionId, roomId=$roomId)"
+ }
+ }
}
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt
index bbc0611083..c857c9e2c8 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt
@@ -16,6 +16,7 @@ import io.element.android.features.call.impl.di.CallBindings
import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.features.call.impl.utils.ActiveCallManager
import io.element.android.libraries.architecture.bindings
+import io.element.android.libraries.di.annotations.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -30,8 +31,8 @@ class DeclineCallBroadcastReceiver : BroadcastReceiver() {
@Inject
lateinit var activeCallManager: ActiveCallManager
- @Inject
- lateinit var appCoroutineScope: CoroutineScope
+ @AppCoroutineScope
+ @Inject lateinit var appCoroutineScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent?) {
val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) }
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt
index 42c2e19b56..b779465a34 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt
@@ -24,6 +24,7 @@ import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import io.element.android.features.call.impl.R
import io.element.android.features.call.impl.ui.ElementCallActivity
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
@@ -78,7 +79,7 @@ class CallForegroundService : Service() {
} else {
0
}
- runCatching {
+ runCatchingExceptions {
ServiceCompat.startForeground(this, notificationId, notification, serviceType)
}.onFailure {
Timber.e(it, "Failed to start ongoing call foreground service")
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt
index 8550f06f3d..363ace4b79 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt
@@ -32,6 +32,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.RoomId
@@ -49,6 +50,7 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
+import timber.log.Timber
import java.util.UUID
class CallScreenPresenter @AssistedInject constructor(
@@ -64,6 +66,7 @@ class CallScreenPresenter @AssistedInject constructor(
private val languageTagProvider: LanguageTagProvider,
private val appForegroundStateService: AppForegroundStateService,
private val activeRoomsHolder: ActiveRoomsHolder,
+ @AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
) : Presenter {
@AssistedFactory
@@ -211,6 +214,7 @@ class CallScreenPresenter @AssistedInject constructor(
theme = theme,
).getOrThrow()
callWidgetDriver.value = result.driver
+ Timber.d("Call widget driver initialized for sessionId: ${inputs.sessionId}, roomId: ${inputs.roomId}")
result.url
}
}
@@ -221,10 +225,12 @@ class CallScreenPresenter @AssistedInject constructor(
private fun HandleMatrixClientSyncState() {
val coroutineScope = rememberCoroutineScope()
DisposableEffect(Unit) {
- val client = (callType as? CallType.RoomCall)?.sessionId?.let {
- matrixClientsProvider.getOrNull(it)
- } ?: return@DisposableEffect onDispose { }
+ val roomCallType = callType as? CallType.RoomCall ?: return@DisposableEffect onDispose {}
+ val client = matrixClientsProvider.getOrNull(roomCallType.sessionId) ?: return@DisposableEffect onDispose {
+ Timber.w("No MatrixClient found for sessionId, can't send call notification: ${roomCallType.sessionId}")
+ }
coroutineScope.launch {
+ Timber.d("Observing sync state in-call for sessionId: ${roomCallType.sessionId}")
client.syncService().syncState
.collect { state ->
if (state == SyncState.Running) {
@@ -235,6 +241,7 @@ class CallScreenPresenter @AssistedInject constructor(
}
}
onDispose {
+ Timber.d("Stopped observing sync state in-call for sessionId: ${roomCallType.sessionId}")
// Make sure we mark the call as ended in the app state
appForegroundStateService.updateIsInCallState(false)
}
@@ -242,12 +249,29 @@ class CallScreenPresenter @AssistedInject constructor(
}
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
- if (!notifiedCallStart) {
- val activeRoomForSession = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
- val sendCallNotificationResult = activeRoomForSession?.sendCallNotificationIfNeeded()
- ?: getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
- sendCallNotificationResult?.onSuccess { notifiedCallStart = true }
+ if (notifiedCallStart) return
+
+ val activeRoomForSession = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
+ val sendCallNotificationResult = if (activeRoomForSession != null) {
+ Timber.d("Notifying call start for room $roomId. Has room call: ${activeRoomForSession.info().hasRoomCall}")
+ activeRoomForSession.sendCallNotificationIfNeeded()
+ } else {
+ // Instantiate the room from the session and roomId and send the notification
+ getJoinedRoom(roomId)?.use { room ->
+ Timber.d("Notifying call start for room $roomId. Has room call: ${room.info().hasRoomCall}")
+ room.sendCallNotificationIfNeeded()
+ } ?: run {
+ Timber.w("No room found for session $sessionId and room $roomId, skipping call notification.")
+ return
+ }
}
+
+ sendCallNotificationResult.fold(
+ onSuccess = { notifiedCallStart = true },
+ onFailure = { error ->
+ Timber.e(error, "Failed to send call notification for room $roomId.")
+ }
+ )
}
private fun parseMessage(message: String): WidgetMessage? {
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt
index 25e856444c..f1aa192d28 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt
@@ -8,10 +8,6 @@
package io.element.android.features.call.impl.ui
import android.annotation.SuppressLint
-import android.content.Context
-import android.media.AudioDeviceCallback
-import android.media.AudioDeviceInfo
-import android.media.AudioManager
import android.util.Log
import android.view.ViewGroup
import android.webkit.ConsoleMessage
@@ -28,6 +24,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -35,17 +32,15 @@ import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.viewinterop.AndroidView
-import androidx.core.content.getSystemService
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.call.impl.R
import io.element.android.features.call.impl.pip.PictureInPictureEvents
import io.element.android.features.call.impl.pip.PictureInPictureState
import io.element.android.features.call.impl.pip.PictureInPictureStateProvider
import io.element.android.features.call.impl.pip.aPictureInPictureState
+import io.element.android.features.call.impl.utils.WebViewAudioManager
import io.element.android.features.call.impl.utils.WebViewPipController
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
-import io.element.android.libraries.androidutils.compat.disableExternalAudioDevice
-import io.element.android.libraries.androidutils.compat.enableExternalAudioDevice
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.button.BackButton
@@ -108,6 +103,8 @@ internal fun CallScreenView(
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
)
} else {
+ var webViewAudioManager by remember { mutableStateOf(null) }
+ val coroutineScope = rememberCoroutineScope()
CallWebView(
modifier = Modifier
.padding(padding)
@@ -120,25 +117,40 @@ internal fun CallScreenView(
val callback: RequestPermissionCallback = { request.grant(it) }
requestPermissions(androidPermissions.toTypedArray(), callback)
},
- onWebViewCreate = { webView ->
+ onCreateWebView = { webView ->
val interceptor = WebViewWidgetMessageInterceptor(
webView = webView,
+ onUrlLoaded = { url ->
+ if (webViewAudioManager?.isInCallMode?.get() == false) {
+ Timber.d("URL $url is loaded, starting in-call audio mode")
+ webViewAudioManager?.onCallStarted()
+ } else {
+ Timber.d("Can't start in-call audio mode since the app is already in it.")
+ }
+ },
onError = { state.eventSink(CallScreenEvents.OnWebViewError(it)) },
)
+ webViewAudioManager = WebViewAudioManager(webView, coroutineScope)
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
val pipController = WebViewPipController(webView)
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController))
+ },
+ onDestroyWebView = {
+ // Reset audio mode
+ webViewAudioManager?.onCallStopped()
}
)
when (state.urlState) {
AsyncData.Uninitialized,
is AsyncData.Loading ->
ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait))
- is AsyncData.Failure ->
+ is AsyncData.Failure -> {
+ Timber.e(state.urlState.error, "WebView failed to load URL: ${state.urlState.error.message}")
ErrorDialog(
content = state.urlState.error.message.orEmpty(),
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
)
+ }
is AsyncData.Success -> Unit
}
}
@@ -150,7 +162,8 @@ private fun CallWebView(
url: AsyncData,
userAgent: String,
onPermissionsRequest: (PermissionRequest) -> Unit,
- onWebViewCreate: (WebView) -> Unit,
+ onCreateWebView: (WebView) -> Unit,
+ onDestroyWebView: (WebView) -> Unit,
modifier: Modifier = Modifier,
) {
if (LocalInspectionMode.current) {
@@ -158,13 +171,11 @@ private fun CallWebView(
Text("WebView - can't be previewed")
}
} else {
- var audioDeviceCallback: AudioDeviceCallback? by remember { mutableStateOf(null) }
AndroidView(
modifier = modifier,
factory = { context ->
- audioDeviceCallback = context.setupAudioConfiguration()
WebView(context).apply {
- onWebViewCreate(this)
+ onCreateWebView(this)
setup(userAgent, onPermissionsRequest)
}
},
@@ -174,41 +185,13 @@ private fun CallWebView(
}
},
onRelease = { webView ->
- // Reset audio mode
- webView.context.releaseAudioConfiguration(audioDeviceCallback)
+ onDestroyWebView(webView)
webView.destroy()
}
)
}
}
-private fun Context.setupAudioConfiguration(): AudioDeviceCallback? {
- val audioManager = getSystemService() ?: return null
- // Set 'voice call' mode so volume keys actually control the call volume
- audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
- audioManager.enableExternalAudioDevice()
- return object : AudioDeviceCallback() {
- override fun onAudioDevicesAdded(addedDevices: Array?) {
- Timber.d("Audio devices added")
- audioManager.enableExternalAudioDevice()
- }
-
- override fun onAudioDevicesRemoved(removedDevices: Array?) {
- Timber.d("Audio devices removed")
- audioManager.enableExternalAudioDevice()
- }
- }.also {
- audioManager.registerAudioDeviceCallback(it, null)
- }
-}
-
-private fun Context.releaseAudioConfiguration(audioDeviceCallback: AudioDeviceCallback?) {
- val audioManager = getSystemService() ?: return
- audioManager.unregisterAudioDeviceCallback(audioDeviceCallback)
- audioManager.disableExternalAudioDevice()
- audioManager.mode = AudioManager.MODE_NORMAL
-}
-
@SuppressLint("SetJavaScriptEnabled")
private fun WebView.setup(
userAgent: String,
@@ -242,6 +225,20 @@ private fun WebView.setup(
ConsoleMessage.MessageLevel.WARNING -> Log.WARN
else -> Log.DEBUG
}
+
+ val message = buildString {
+ append(consoleMessage.sourceId())
+ append(":")
+ append(consoleMessage.lineNumber())
+ append(" ")
+ append(consoleMessage.message())
+ }
+
+ if (message.contains("password=")) {
+ // Avoid logging any messages that contain "password" to prevent leaking sensitive information
+ return true
+ }
+
Timber.tag("WebView").log(
priority = priority,
message = buildString {
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
index 4c44fb29d9..870c224464 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
@@ -81,13 +81,14 @@ class ElementCallActivity :
applicationContext.bindings().inject(this)
- @Suppress("DEPRECATION")
- window.addFlags(
- WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
- WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON or
- WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
- WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
- )
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ setShowWhenLocked(true)
+ } else {
+ @Suppress("DEPRECATION")
+ window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
+ }
setCallType(intent)
// If presenter is not created at this point, it means we have no call to display, the Activity is finishing, so return early
@@ -97,6 +98,8 @@ class ElementCallActivity :
pictureInPicturePresenter.setPipView(this)
+ Timber.d("Created ElementCallActivity with call type: ${webViewTarget.value}")
+
setContent {
val pipState = pictureInPicturePresenter.present()
ListenToAndroidEvents(pipState)
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt
index c5162d9a9e..dc77af9b77 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt
@@ -23,6 +23,7 @@ import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.theme.ElementThemeApp
+import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filter
@@ -57,8 +58,8 @@ class IncomingCallActivity : AppCompatActivity() {
@Inject
lateinit var buildMeta: BuildMeta
- @Inject
- lateinit var appCoroutineScope: CoroutineScope
+ @AppCoroutineScope
+ @Inject lateinit var appCoroutineScope: CoroutineScope
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt
index 8804a91f79..d4f5abb59d 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt
@@ -21,9 +21,11 @@ import io.element.android.features.call.api.CallType
import io.element.android.features.call.api.CurrentCall
import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
+import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
@@ -86,6 +88,7 @@ interface ActiveCallManager {
@ContributesBinding(AppScope::class)
class DefaultActiveCallManager @Inject constructor(
@ApplicationContext context: Context,
+ @AppCoroutineScope
private val coroutineScope: CoroutineScope,
private val onMissedCallNotificationHandler: OnMissedCallNotificationHandler,
private val ringingCallNotificationCreator: RingingCallNotificationCreator,
@@ -180,6 +183,9 @@ class DefaultActiveCallManager @Inject constructor(
Timber.tag(tag).w("Call type $callType does not match the active call type, ignoring")
return
}
+
+ Timber.tag(tag).d("Hung up call: $callType")
+
cancelIncomingCallNotification()
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after hang up")
@@ -190,6 +196,8 @@ class DefaultActiveCallManager @Inject constructor(
}
override suspend fun joinedCall(callType: CallType) = mutex.withLock {
+ Timber.tag(tag).d("Joined call: $callType")
+
cancelIncomingCallNotification()
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after joining call")
@@ -218,7 +226,7 @@ class DefaultActiveCallManager @Inject constructor(
timestamp = notificationData.timestamp,
textContent = notificationData.textContent,
) ?: return
- runCatching {
+ runCatchingExceptions {
notificationManagerCompat.notify(
NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.INCOMING_CALL),
notification,
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt
index dd4de7abb5..d87826fe46 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt
@@ -8,6 +8,7 @@
package io.element.android.features.call.impl.utils
import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.RoomId
@@ -33,7 +34,7 @@ class DefaultCallWidgetProvider @Inject constructor(
clientId: String,
languageTag: String?,
theme: String?,
- ): Result = runCatching {
+ ): Result = runCatchingExceptions {
val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow()
val room = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
?: matrixClient.getJoinedRoom(roomId)
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt
new file mode 100644
index 0000000000..d29843a70e
--- /dev/null
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt
@@ -0,0 +1,450 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.call.impl.utils
+
+import android.content.Context
+import android.media.AudioDeviceCallback
+import android.media.AudioDeviceInfo
+import android.media.AudioManager
+import android.os.Build
+import android.os.PowerManager
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import androidx.core.content.getSystemService
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import kotlinx.serialization.json.Json
+import timber.log.Timber
+import java.util.concurrent.Executors
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * This class manages the audio devices for a WebView.
+ *
+ * It listens for audio device changes and updates the WebView with the available devices.
+ * It also handles the selection of the audio device by the user in the WebView and the default audio device based on the device type.
+ *
+ * See also: [Element Call controls docs.](https://github.com/element-hq/element-call/blob/livekit/docs/controls.md#audio-devices)
+ */
+class WebViewAudioManager(
+ private val webView: WebView,
+ private val coroutineScope: CoroutineScope,
+) {
+ // The list of device types that are considered as communication devices, sorted by likelihood of it being used for communication.
+ private val wantedDeviceTypes = listOf(
+ // Paired bluetooth device with microphone
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
+ // USB devices which can play or record audio
+ AudioDeviceInfo.TYPE_USB_HEADSET,
+ AudioDeviceInfo.TYPE_USB_DEVICE,
+ AudioDeviceInfo.TYPE_USB_ACCESSORY,
+ // Wired audio devices
+ AudioDeviceInfo.TYPE_WIRED_HEADSET,
+ AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
+ // The built-in speaker of the device
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
+ // The built-in earpiece of the device
+ AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
+ )
+
+ private val audioManager = webView.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+
+ private val proximitySensorWakeLock by lazy {
+ webView.context.getSystemService()
+ ?.takeIf { it.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK) }
+ ?.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "${webView.context.packageName}:ProximitySensorCallWakeLock")
+ }
+
+ /**
+ * This listener tracks the current communication device and updates the WebView when it changes.
+ */
+ private val commsDeviceChangedListener = AudioManager.OnCommunicationDeviceChangedListener { device ->
+ if (device != null && device.id == expectedNewCommunicationDeviceId) {
+ expectedNewCommunicationDeviceId = null
+ Timber.d("Audio device changed, type: ${device.type}")
+ updateSelectedAudioDeviceInWebView(device.id.toString())
+ } else if (device != null && device.id != expectedNewCommunicationDeviceId) {
+ // We were expecting a device change but it didn't happen, so we should retry
+ val expectedDeviceId = expectedNewCommunicationDeviceId
+ if (expectedDeviceId != null) {
+ // Remove the expected id so we only retry once
+ expectedNewCommunicationDeviceId = null
+ audioManager.selectAudioDevice(expectedDeviceId.toString())
+ }
+ } else {
+ Timber.d("Audio device cleared")
+ expectedNewCommunicationDeviceId = null
+ audioManager.selectAudioDevice(null)
+ }
+ }
+
+ /**
+ * This callback is used to listen for audio device changes coming from the OS.
+ */
+ private val audioDeviceCallback = object : AudioDeviceCallback() {
+ override fun onAudioDevicesAdded(addedDevices: Array?) {
+ val validNewDevices = addedDevices.orEmpty().filter { it.type in wantedDeviceTypes && it.isSink }
+ if (validNewDevices.isEmpty()) return
+
+ // We need to calculate the available devices ourselves, since calling `listAudioDevices` will return an outdated list
+ val audioDevices = (listAudioDevices() + validNewDevices).distinctBy { it.id }
+ setAvailableAudioDevices(audioDevices.map(SerializableAudioDevice::fromAudioDeviceInfo))
+ // This should automatically switch to a new device if it has a higher priority than the current one
+ selectDefaultAudioDevice(audioDevices)
+ }
+
+ override fun onAudioDevicesRemoved(removedDevices: Array?) {
+ // Update the available devices
+ setAvailableAudioDevices()
+
+ // Unless the removed device is the current one, we don't need to do anything else
+ val removedCurrentDevice = removedDevices.orEmpty().any { it.id == currentDeviceId }
+ if (!removedCurrentDevice) return
+
+ val previousDevice = previousSelectedDevice
+ if (previousDevice != null) {
+ previousSelectedDevice = null
+ // If we have a previous device, we should select it again
+ audioManager.selectAudioDevice(previousDevice.id.toString())
+ } else {
+ // If we don't have a previous device, we should select the default one
+ selectDefaultAudioDevice()
+ }
+ }
+ }
+
+ /**
+ * The currently used audio device id.
+ */
+ private var currentDeviceId: Int? = null
+
+ /**
+ * When a new audio device is selected but not yet set as the communication device by the OS, this id is used to check if the device is the expected one.
+ */
+ private var expectedNewCommunicationDeviceId: Int? = null
+
+ /**
+ * Previously selected device, used to restore the selection when the selected device is removed.
+ */
+ private var previousSelectedDevice: AudioDeviceInfo? = null
+
+ private var hasRegisteredCallbacks = false
+
+ /**
+ * Marks if the WebView audio is in call mode or not.
+ */
+ val isInCallMode = AtomicBoolean(false)
+
+ init {
+ // Apparently, registering the javascript interface takes a while, so registering and immediately using it doesn't work
+ // We register it ahead of time to avoid this issue
+ registerWebViewDeviceSelectedCallback()
+ }
+
+ /**
+ * Call this method when the call starts to enable in-call audio mode.
+ *
+ * It'll set the audio mode to [AudioManager.MODE_IN_COMMUNICATION] if possible, register the audio device callback and set the available audio devices.
+ */
+ fun onCallStarted() {
+ if (!isInCallMode.compareAndSet(false, true)) {
+ Timber.w("Audio: tried to enable webview in-call audio mode while already in it")
+ return
+ }
+
+ Timber.d("Audio: enabling webview in-call audio mode")
+
+ audioManager.mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ // Set 'voice call' mode so volume keys actually control the call volume
+ AudioManager.MODE_IN_COMMUNICATION
+ } else {
+ // Workaround for Android 12 and lower, otherwise changing the audio device doesn't work
+ AudioManager.MODE_NORMAL
+ }
+
+ setWebViewAndroidNativeBridge()
+ }
+
+ /**
+ * Call this method when the call stops to disable in-call audio mode.
+ *
+ * It's the counterpart of [onCallStarted], and should be called as a pair with it once the call has ended.
+ */
+ fun onCallStopped() {
+ if (!isInCallMode.compareAndSet(true, false)) {
+ Timber.w("Audio: tried to disable webview in-call audio mode while already disabled")
+ return
+ }
+
+ if (proximitySensorWakeLock?.isHeld == true) {
+ proximitySensorWakeLock?.release()
+ }
+
+ audioManager.mode = AudioManager.MODE_NORMAL
+
+ if (!hasRegisteredCallbacks) {
+ Timber.w("Audio: tried to disable webview in-call audio mode without registering callbacks")
+ return
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ audioManager.clearCommunicationDevice()
+ audioManager.removeOnCommunicationDeviceChangedListener(commsDeviceChangedListener)
+ }
+
+ audioManager.unregisterAudioDeviceCallback(audioDeviceCallback)
+ }
+
+ /**
+ * Registers the WebView audio device selected callback.
+ *
+ * This should be called when the WebView is created to ensure that the callback is set before any audio device selection is made.
+ */
+ private fun registerWebViewDeviceSelectedCallback() {
+ val webViewAudioDeviceSelectedCallback = AndroidWebViewAudioBridge(
+ onAudioDeviceSelected = { selectedDeviceId ->
+ Timber.d("Audio device selected in webview, id: $selectedDeviceId")
+ previousSelectedDevice = listAudioDevices().find { it.id.toString() == selectedDeviceId }
+ audioManager.selectAudioDevice(selectedDeviceId)
+ },
+ onAudioPlaybackStarted = {
+ coroutineScope.launch(Dispatchers.Main) {
+ // Calling this ahead of time makes the default audio device to not use the right audio stream
+ setAvailableAudioDevices()
+
+ // Registering the audio devices changed callback will also set the default audio device
+ audioManager.registerAudioDeviceCallback(audioDeviceCallback, null)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ audioManager.addOnCommunicationDeviceChangedListener(Executors.newSingleThreadExecutor(), commsDeviceChangedListener)
+ }
+
+ hasRegisteredCallbacks = true
+ }
+ }
+ )
+ Timber.d("Setting androidNativeBridge javascript interface in webview")
+ webView.addJavascriptInterface(webViewAudioDeviceSelectedCallback, "androidNativeBridge")
+ }
+
+ /**
+ * Assigns the callback in the WebView to be called when the user selects an audio device.
+ *
+ * It should be called with some delay after [registerWebViewDeviceSelectedCallback].
+ */
+ private fun setWebViewAndroidNativeBridge() {
+ Timber.d("Adding callback in controls.onAudioPlaybackStarted")
+ webView.evaluateJavascript("controls.onAudioPlaybackStarted = () => { androidNativeBridge.onTrackReady(); };", null)
+ Timber.d("Adding callback in controls.onOutputDeviceSelect")
+ webView.evaluateJavascript("controls.onOutputDeviceSelect = (id) => { androidNativeBridge.setOutputDevice(id); };", null)
+ }
+
+ /**
+ * Returns the list of available audio devices.
+ *
+ * On Android 11 ([Build.VERSION_CODES.R]) and lower, it returns the list of output devices as a fallback.
+ */
+ private fun listAudioDevices(): List {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ audioManager.availableCommunicationDevices
+ } else {
+ val rawAudioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
+ rawAudioDevices.filter { it.type in wantedDeviceTypes && it.isSink }
+ }
+ }
+
+ /**
+ * Sets the available audio devices in the WebView.
+ *
+ * @param devices The list of audio devices to set. If not provided, it will use the current list of audio devices.
+ */
+ private fun setAvailableAudioDevices(
+ devices: List = listAudioDevices().map(SerializableAudioDevice::fromAudioDeviceInfo),
+ ) {
+ Timber.d("Updating available audio devices")
+ val jsonSerializer = Json {
+ encodeDefaults = true
+ explicitNulls = false
+ }
+ val deviceList = jsonSerializer.encodeToString(devices)
+ webView.evaluateJavascript("controls.setAvailableOutputDevices($deviceList);", {
+ Timber.d("Audio: setAvailableOutputDevices result: $it")
+ })
+ }
+
+ /**
+ * Selects the default audio device based on the available devices.
+ *
+ * @param availableDevices The list of available audio devices to select from. If not provided, it will use the current list of audio devices.
+ */
+ private fun selectDefaultAudioDevice(availableDevices: List = listAudioDevices()) {
+ val selectedDevice = availableDevices.minByOrNull {
+ wantedDeviceTypes.indexOf(it.type).let { index ->
+ // If the device type is not in the wantedDeviceTypes list, we give it a low priority
+ if (index == -1) Int.MAX_VALUE else index
+ }
+ }
+
+ expectedNewCommunicationDeviceId = selectedDevice?.id
+ audioManager.selectAudioDevice(selectedDevice)
+
+ selectedDevice?.let {
+ updateSelectedAudioDeviceInWebView(it.id.toString())
+ } ?: run {
+ Timber.w("Audio: unable to select default audio device")
+ }
+ }
+
+ /**
+ * Updates the WebView's UI to reflect the selected audio device.
+ *
+ * @param deviceId The id of the selected audio device.
+ */
+ private fun updateSelectedAudioDeviceInWebView(deviceId: String) {
+ coroutineScope.launch(Dispatchers.Main) {
+ webView.evaluateJavascript("controls.setOutputDevice('$deviceId');", null)
+ }
+ }
+
+ /**
+ * Selects the audio device on the OS based on the provided device id.
+ *
+ * It will select the device only if it is available in the list of audio devices.
+ *
+ * @param device The id of the audio device to select.
+ */
+ private fun AudioManager.selectAudioDevice(device: String) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val audioDevice = availableCommunicationDevices.find { it.id.toString() == device }
+ selectAudioDevice(audioDevice)
+ } else {
+ val rawAudioDevices = getDevices(AudioManager.GET_DEVICES_OUTPUTS)
+ val audioDevice = rawAudioDevices.find { it.id.toString() == device }
+ selectAudioDevice(audioDevice)
+ }
+ }
+
+ /**
+ * Selects the audio device on the OS based on the provided device info.
+ *
+ * @param device The info of the audio device to select, or none to clear the selected device.
+ */
+ @Suppress("DEPRECATION")
+ private fun AudioManager.selectAudioDevice(device: AudioDeviceInfo?) {
+ currentDeviceId = device?.id
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ if (device != null) {
+ Timber.d("Setting communication device: ${device.id} - ${deviceName(device.type, device.productName.toString())}")
+ setCommunicationDevice(device)
+ } else {
+ audioManager.clearCommunicationDevice()
+ }
+ } else {
+ // On Android 11 and lower, we don't have the concept of communication devices
+ // We have to call the right methods based on the device type
+ if (device != null) {
+ isSpeakerphoneOn = device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
+ isBluetoothScoOn = device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
+ } else {
+ isSpeakerphoneOn = false
+ isBluetoothScoOn = false
+ }
+ }
+
+ expectedNewCommunicationDeviceId = null
+
+ @Suppress("WakeLock", "WakeLockTimeout")
+ if (device?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE && proximitySensorWakeLock?.isHeld == false) {
+ // If the device is the built-in earpiece, we need to acquire the proximity sensor wake lock
+ proximitySensorWakeLock?.acquire()
+ } else if (proximitySensorWakeLock?.isHeld == true) {
+ // If the device is no longer the earpiece, we need to release the wake lock
+ proximitySensorWakeLock?.release()
+ }
+ }
+}
+
+/**
+ * This class is used to handle the audio device selection in the WebView.
+ * It listens for the audio device selection event and calls the callback with the selected device ID.
+ */
+private class AndroidWebViewAudioBridge(
+ private val onAudioDeviceSelected: (String) -> Unit,
+ private val onAudioPlaybackStarted: () -> Unit,
+) {
+ @JavascriptInterface
+ fun setOutputDevice(id: String) {
+ Timber.d("Audio device selected in webview, id: $id")
+ onAudioDeviceSelected(id)
+ }
+
+ @JavascriptInterface
+ fun onTrackReady() {
+ // This method can be used to notify the WebView that the audio track is ready
+ // It can be used to start playing audio or to update the UI
+ Timber.d("Audio track is ready")
+
+ onAudioPlaybackStarted()
+ }
+}
+
+private fun deviceName(type: Int, name: String): String {
+ // TODO maybe translate these?
+ val typePart = when (type) {
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "Bluetooth"
+ AudioDeviceInfo.TYPE_USB_ACCESSORY -> "USB accessory"
+ AudioDeviceInfo.TYPE_USB_DEVICE -> "USB device"
+ AudioDeviceInfo.TYPE_USB_HEADSET -> "USB headset"
+ AudioDeviceInfo.TYPE_WIRED_HEADSET -> "Wired headset"
+ AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> "Wired headphones"
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "Built-in speaker"
+ AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "Built-in earpiece"
+ else -> "Unknown"
+ }
+ return if (isBuiltIn(type)) {
+ typePart
+ } else {
+ "$typePart - $name"
+ }
+}
+
+private fun isBuiltIn(type: Int): Boolean = when (type) {
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
+ AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
+ AudioDeviceInfo.TYPE_BUILTIN_MIC,
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE -> true
+ else -> false
+}
+
+/**
+ * This class is used to serialize the audio device information to JSON.
+ */
+@Suppress("unused")
+@Serializable
+internal data class SerializableAudioDevice(
+ val id: String,
+ val name: String,
+ @Transient val type: Int = 0,
+ // These have to be part of the constructor for the JSON serializer to pick them up
+ val isEarpiece: Boolean = type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
+ val isSpeaker: Boolean = type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
+ val isExternalHeadset: Boolean = type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
+) {
+ companion object {
+ fun fromAudioDeviceInfo(audioDeviceInfo: AudioDeviceInfo): SerializableAudioDevice {
+ return SerializableAudioDevice(
+ id = audioDeviceInfo.id.toString(),
+ name = deviceName(type = audioDeviceInfo.type, name = audioDeviceInfo.productName.toString()),
+ type = audioDeviceInfo.type,
+ )
+ }
+ }
+}
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt
index 8fc5d8b7d6..55adc246e9 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt
@@ -26,6 +26,7 @@ import timber.log.Timber
class WebViewWidgetMessageInterceptor(
private val webView: WebView,
+ private val onUrlLoaded: (String) -> Unit,
private val onError: (String?) -> Unit,
) : WidgetMessageInterceptor {
companion object {
@@ -44,13 +45,13 @@ class WebViewWidgetMessageInterceptor(
.build()
webView.webViewClient = object : WebViewClient() {
- override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+ override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
// Due to https://github.com/element-hq/element-x-android/issues/4097
// we need to supply a logging implementation that correctly includes
// objects in log lines.
- view?.evaluateJavascript(
+ view.evaluateJavascript(
"""
function logFn(consoleLogFn, ...args) {
consoleLogFn(
@@ -72,7 +73,7 @@ class WebViewWidgetMessageInterceptor(
// This listener will receive both messages:
// - EC widget API -> Element X (message.data.api == "fromWidget")
// - Element X -> EC widget API (message.data.api == "toWidget"), we should ignore these
- view?.evaluateJavascript(
+ view.evaluateJavascript(
"""
window.addEventListener('message', function(event) {
let message = {data: event.data, origin: event.origin}
@@ -90,6 +91,10 @@ class WebViewWidgetMessageInterceptor(
)
}
+ override fun onPageFinished(view: WebView, url: String) {
+ onUrlLoaded(url)
+ }
+
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
// No network for instance, transmit the error
Timber.e("onReceivedError error: ${error?.errorCode} ${error?.description}")
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt
index a78965a87f..a21cadab7a 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt
@@ -8,13 +8,14 @@
package io.element.android.features.call.impl.utils
import io.element.android.features.call.impl.data.WidgetMessage
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import kotlinx.serialization.json.Json
object WidgetMessageSerializer {
private val coder = Json { ignoreUnknownKeys = true }
fun deserialize(message: String): Result {
- return runCatching { coder.decodeFromString(WidgetMessage.serializer(), message) }
+ return runCatchingExceptions { coder.decodeFromString(WidgetMessage.serializer(), message) }
}
fun serialize(message: WidgetMessage): String {
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt
index 38a3af46ed..9f2d59e3b6 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt
@@ -35,7 +35,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider>(AsyncAction.Uninitialized)
+ action.execute(aMatrixUser(), true, state)
+ assertThat(state.value).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
+ assertThat(analyticsService.capturedEvents).isEmpty()
+ }
+
@Test
fun `when dm is not found, assert dm is created, state is updated with given room id and analytics get called`() = runTest {
val matrixClient = FakeMatrixClient().apply {
- givenFindDmResult(null)
+ givenFindDmResult(Result.success(null))
givenCreateDmResult(Result.success(A_ROOM_ID))
}
val analyticsService = FakeAnalyticsService()
@@ -54,7 +67,7 @@ class DefaultStartDMActionTest {
@Test
fun `when dm is not found, and createIfDmDoesNotExist is false, assert dm is not created and state is updated to confirmation state`() = runTest {
val matrixClient = FakeMatrixClient().apply {
- givenFindDmResult(null)
+ givenFindDmResult(Result.success(null))
givenCreateDmResult(Result.success(A_ROOM_ID))
}
val analyticsService = FakeAnalyticsService()
@@ -69,14 +82,14 @@ class DefaultStartDMActionTest {
@Test
fun `when dm creation fails, assert state is updated with given error`() = runTest {
val matrixClient = FakeMatrixClient().apply {
- givenFindDmResult(null)
- givenCreateDmResult(Result.failure(A_THROWABLE))
+ givenFindDmResult(Result.success(null))
+ givenCreateDmResult(Result.failure(AN_EXCEPTION))
}
val analyticsService = FakeAnalyticsService()
val action = createStartDMAction(matrixClient, analyticsService)
val state = mutableStateOf>(AsyncAction.Uninitialized)
action.execute(aMatrixUser(), true, state)
- assertThat(state.value).isEqualTo(AsyncAction.Failure(A_THROWABLE))
+ assertThat(state.value).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
assertThat(analyticsService.capturedEvents).isEmpty()
}
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureBaseRoomPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureBaseRoomPresenterTest.kt
index d8a7085f0d..a0a4783093 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/createroom/impl/configureroom/ConfigureBaseRoomPresenterTest.kt
@@ -22,10 +22,10 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
-import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
import io.element.android.libraries.matrix.ui.components.aMatrixUser
@@ -274,7 +274,7 @@ class ConfigureBaseRoomPresenterTest {
createRoomDataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY))
skipItems(1)
mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk())))
- matrixClient.givenUploadMediaResult(Result.failure(A_THROWABLE))
+ matrixClient.givenUploadMediaResult(Result.failure(AN_EXCEPTION))
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
@@ -298,7 +298,7 @@ class ConfigureBaseRoomPresenterTest {
)
presenter.test {
val initialState = initialState()
- val createRoomResult = Result.failure(A_THROWABLE)
+ val createRoomResult = Result.failure(AN_EXCEPTION)
fakeMatrixClient.givenCreateRoomResult(createRoomResult)
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt
index 93db738d35..b2e75c9f5a 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt
+++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt
@@ -17,6 +17,7 @@ import io.element.android.features.createroom.impl.R
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.setSafeContent
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
@@ -56,7 +57,7 @@ class JoinBaseRoomByAddressViewTest {
private fun AndroidComposeTestRule.setJoinRoomByAddressView(
state: JoinRoomByAddressState,
) {
- setContent {
+ setSafeContent {
JoinRoomByAddressView(state = state)
}
}
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootPresenterTest.kt
index 54cd1d201c..d2d9959e2a 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootPresenterTest.kt
+++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootPresenterTest.kt
@@ -24,8 +24,8 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
-import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.usersearch.test.FakeUserRepository
import io.element.android.tests.testutils.WarmUpRule
@@ -42,7 +42,7 @@ class CreateBaseRoomRootPresenterTest {
@Test
fun `present - start DM action failure scenario`() = runTest {
- val startDMFailureResult = AsyncAction.Failure(A_THROWABLE)
+ val startDMFailureResult = AsyncAction.Failure(AN_EXCEPTION)
val executeResult = lambdaRecorder>, Unit> { _, _, actionState ->
actionState.value = startDMFailureResult
}
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt
index b7d9ab4ba1..9e61f030c5 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt
@@ -34,10 +34,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.AutofillType
+import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentType
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
@@ -54,7 +56,6 @@ import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrgani
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.components.list.SwitchListItem
-import io.element.android.libraries.designsystem.modifiers.autofill
import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -276,14 +277,9 @@ private fun Content(
.fillMaxWidth()
.onTabOrEnterKeyFocusNext(focusManager)
.testTag(TestTags.loginPassword)
- .autofill(
- autofillTypes = listOf(AutofillType.Password),
- onFill = {
- val sanitized = it.sanitize()
- passwordFieldState = sanitized
- eventSink(AccountDeactivationEvents.SetPassword(sanitized))
- }
- ),
+ .semantics {
+ contentType = ContentType.Password
+ },
onValueChange = {
val sanitized = it.sanitize()
passwordFieldState = sanitized
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt
index 05bd14efcd..5db0955c5a 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt
@@ -16,6 +16,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
@@ -27,6 +28,7 @@ import kotlinx.coroutines.launch
class NotificationsOptInPresenter @AssistedInject constructor(
permissionsPresenterFactory: PermissionsPresenter.Factory,
@Assisted private val callback: NotificationsOptInNode.Callback,
+ @AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
private val permissionStateProvider: PermissionStateProvider,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt
index b0450975af..4c33f9e48f 100644
--- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt
+++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt
@@ -29,10 +29,10 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider, JoinedRoom.Trigger, Result> { _, _, _ ->
- Result.failure(Throwable("Join room failed"))
+ Result.failure(RuntimeException("Join room failed"))
}
val acceptInvite = DefaultAcceptInvite(
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt
index cbe710b2dd..7d3b64f6f4 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt
@@ -39,10 +39,8 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
-import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.api.exception.ErrorKind
-import io.element.android.libraries.matrix.api.getRoomInfoFlow
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember
@@ -88,7 +86,7 @@ class JoinRoomPresenter @AssistedInject constructor(
val coroutineScope = rememberCoroutineScope()
var retryCount by remember { mutableIntStateOf(0) }
val roomInfo by remember {
- matrixClient.getRoomInfoFlow(roomId.toRoomIdOrAlias())
+ matrixClient.getRoomInfoFlow(roomId)
}.collectAsState(initial = Optional.empty())
val joinAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val knockAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) }
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 006cc986ab..c751dc971f 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
@@ -45,10 +45,10 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta
+import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.room.aRoomPreview
import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo
-import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
import io.element.android.libraries.matrix.ui.model.InviteSender
import io.element.android.libraries.matrix.ui.model.toInviteSender
@@ -88,12 +88,12 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is joined then content state is filled with his data`() = runTest {
- val roomSummary = aRoomSummary()
+ val roomInfo = aRoomInfo()
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
).apply {
- getRoomSummaryFlowLambda = { _ ->
- flowOf(Optional.of(roomSummary))
+ getRoomInfoFlowLambda = { _ ->
+ flowOf(Optional.of(roomInfo))
}
}
val presenter = createJoinRoomPresenter(
@@ -104,24 +104,24 @@ class JoinRoomPresenterTest {
awaitItem().also { state ->
val contentState = state.contentState as ContentState.Loaded
assertThat(contentState.roomId).isEqualTo(A_ROOM_ID)
- assertThat(contentState.name).isEqualTo(roomSummary.info.name)
- assertThat(contentState.topic).isEqualTo(roomSummary.info.topic)
- assertThat(contentState.alias).isEqualTo(roomSummary.info.canonicalAlias)
- assertThat(contentState.numberOfMembers).isEqualTo(roomSummary.info.joinedMembersCount)
- assertThat(contentState.isDm).isEqualTo(roomSummary.info.isDirect)
- assertThat(contentState.roomAvatarUrl).isEqualTo(roomSummary.info.avatarUrl)
+ assertThat(contentState.name).isEqualTo(roomInfo.name)
+ assertThat(contentState.topic).isEqualTo(roomInfo.topic)
+ assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias)
+ assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.joinedMembersCount)
+ assertThat(contentState.isDm).isEqualTo(roomInfo.isDirect)
+ assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl)
}
}
}
@Test
fun `present - when room is invited then join authorization is equal to invited`() = runTest {
- val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.INVITED)
+ val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
).apply {
- getRoomSummaryFlowLambda = { _ ->
- flowOf(Optional.of(roomSummary))
+ getRoomInfoFlowLambda = { _ ->
+ flowOf(Optional.of(roomInfo))
}
}
val seenInvitesStore = InMemorySeenInvitesStore()
@@ -129,7 +129,7 @@ class JoinRoomPresenterTest {
matrixClient = matrixClient,
seenInvitesStore = seenInvitesStore,
)
- val inviteData = roomSummary.info.toInviteData()
+ val inviteData = roomInfo.toInviteData()
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
presenter.test {
skipItems(2)
@@ -137,7 +137,7 @@ class JoinRoomPresenterTest {
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(inviteData, null))
}
// Check that the roomId is stored in the seen invites store
- assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(roomSummary.roomId)
+ assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(roomInfo.id)
}
}
@@ -145,17 +145,17 @@ class JoinRoomPresenterTest {
fun `present - when room is invited then join authorization is equal to invited, an inviter is provided`() = runTest {
val inviter = aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob")
val expectedInviteSender = inviter.toInviteSender()
- val roomSummary = aRoomSummary(
+ val roomInfo = aRoomInfo(
currentUserMembership = CurrentUserMembership.INVITED,
joinedMembersCount = 5,
inviter = inviter,
)
- val inviteData = roomSummary.info.toInviteData()
+ val inviteData = roomInfo.toInviteData()
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
).apply {
- getRoomSummaryFlowLambda = { _ ->
- flowOf(Optional.of(roomSummary))
+ getRoomInfoFlowLambda = { _ ->
+ flowOf(Optional.of(roomInfo))
}
}
val presenter = createJoinRoomPresenter(
@@ -172,7 +172,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is invited read the number of member from the room preview`() = runTest {
- val roomSummary = aRoomSummary(
+ val roomInfo = aRoomInfo(
currentUserMembership = CurrentUserMembership.INVITED,
// It seems that the SDK does not provide this value.
joinedMembersCount = 0,
@@ -188,8 +188,8 @@ class JoinRoomPresenterTest {
)
},
).apply {
- getRoomSummaryFlowLambda = { _ ->
- flowOf(Optional.of(roomSummary))
+ getRoomInfoFlowLambda = { _ ->
+ flowOf(Optional.of(roomInfo))
}
}
val presenter = createJoinRoomPresenter(
@@ -209,13 +209,13 @@ class JoinRoomPresenterTest {
val acceptDeclinePresenter = Presenter {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
}
- val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.INVITED)
+ val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED)
val matrixClient = FakeMatrixClient().apply {
- getRoomSummaryFlowLambda = { _ ->
- flowOf(Optional.of(roomSummary))
+ getRoomInfoFlowLambda = { _ ->
+ flowOf(Optional.of(roomInfo))
}
}
- val inviteData = roomSummary.info.toInviteData()
+ val inviteData = roomInfo.toInviteData()
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient,
acceptDeclineInvitePresenter = acceptDeclinePresenter
@@ -324,7 +324,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is banned, then join authorization is equal to IsBanned`() = runTest {
- val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.BANNED, joinRule = JoinRule.Public)
+ val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.BANNED, joinRule = JoinRule.Public)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.success(
@@ -346,8 +346,8 @@ class JoinRoomPresenterTest {
)
}
).apply {
- getRoomSummaryFlowLambda = { _ ->
- flowOf(Optional.of(roomSummary))
+ getRoomInfoFlowLambda = { _ ->
+ flowOf(Optional.of(roomInfo))
}
}
val presenter = createJoinRoomPresenter(
@@ -369,12 +369,12 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is left and public then join authorization is equal to canJoin`() = runTest {
- val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, joinRule = JoinRule.Public)
+ val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, joinRule = JoinRule.Public)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
).apply {
- getRoomSummaryFlowLambda = { _ ->
- flowOf(Optional.of(roomSummary))
+ getRoomInfoFlowLambda = { _ ->
+ flowOf(Optional.of(roomInfo))
}
}
val presenter = createJoinRoomPresenter(
@@ -390,12 +390,12 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is left and join rule null then join authorization is equal to Unknown`() = runTest {
- val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, joinRule = null)
+ val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, joinRule = null)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
).apply {
- getRoomSummaryFlowLambda = { _ ->
- flowOf(Optional.of(roomSummary))
+ getRoomInfoFlowLambda = { _ ->
+ flowOf(Optional.of(roomInfo))
}
}
val presenter = createJoinRoomPresenter(
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt
index 204a9dc4d6..737a3b0562 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt
@@ -18,6 +18,7 @@ import io.element.android.features.knockrequests.impl.data.KnockRequestPresentab
import io.element.android.features.knockrequests.impl.data.KnockRequestsService
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.mapState
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
@@ -28,7 +29,8 @@ private const val ACCEPT_ERROR_DISPLAY_DURATION = 1500L
class KnockRequestsBannerPresenter @Inject constructor(
private val knockRequestsService: KnockRequestsService,
- private val appCoroutineScope: CoroutineScope,
+ @SessionCoroutineScope
+ private val sessionCoroutineScope: CoroutineScope,
) : Presenter {
@Composable
override fun present(): KnockRequestsBannerState {
@@ -52,13 +54,13 @@ class KnockRequestsBannerPresenter @Inject constructor(
fun handleEvents(event: KnockRequestsBannerEvents) {
when (event) {
is KnockRequestsBannerEvents.AcceptSingleRequest -> {
- appCoroutineScope.acceptSingleKnockRequest(
+ sessionCoroutineScope.acceptSingleKnockRequest(
knockRequests = knockRequests,
displayAcceptError = showAcceptError,
)
}
is KnockRequestsBannerEvents.Dismiss -> {
- appCoroutineScope.launch {
+ sessionCoroutineScope.launch {
knockRequestsService.markAllKnockRequestsAsSeen()
}
}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt
index b56ed8af4e..a83450e9bd 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt
@@ -89,7 +89,7 @@ open class KnockRequestsListStateProvider : PreviewParameterProvider()
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt
index 97dd875b01..9f44b03a10 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt
@@ -20,6 +20,7 @@ import io.element.android.features.lockscreen.impl.biometric.BiometricAuthentica
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.di.annotations.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -29,6 +30,7 @@ class LockScreenSettingsPresenter @Inject constructor(
private val pinCodeManager: PinCodeManager,
private val lockScreenStore: LockScreenStore,
private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
+ @AppCoroutineScope
private val coroutineScope: CoroutineScope,
) : Presenter {
@Composable
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt
index 9f3bf082e2..707ca6b710 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt
@@ -26,6 +26,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse
+import io.element.android.libraries.di.annotations.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -34,6 +35,7 @@ class PinUnlockPresenter @Inject constructor(
private val pinCodeManager: PinCodeManager,
private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
private val logoutUseCase: LogoutUseCase,
+ @AppCoroutineScope
private val coroutineScope: CoroutineScope,
private val pinUnlockHelper: PinUnlockHelper,
) : Presenter {
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
index fb21367501..2d791f0f9a 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
@@ -36,6 +36,7 @@ import io.element.android.features.login.impl.screens.createaccount.CreateAccoun
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
import io.element.android.features.login.impl.screens.onboarding.OnBoardingNode
import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode
+import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs
@@ -45,7 +46,6 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
-import io.element.android.libraries.oidc.api.OidcEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@@ -57,7 +57,6 @@ class LoginFlowNode @AssistedInject constructor(
private val accountProviderDataSource: AccountProviderDataSource,
private val defaultLoginUserStory: DefaultLoginUserStory,
private val oidcActionFlow: OidcActionFlow,
- private val oidcEntryPoint: OidcEntryPoint,
) : BaseFlowNode(
backstack = BackStack(
initialElement = NavTarget.OnBoarding,
@@ -74,15 +73,15 @@ class LoginFlowNode @AssistedInject constructor(
private var activity: Activity? = null
private var darkTheme: Boolean = false
- private var customChromeTabStarted = false
+ private var externalAppStarted = false
override fun onBuilt() {
super.onBuilt()
defaultLoginUserStory.setLoginFlowIsDone(false)
lifecycle.subscribe(
onResume = {
- if (customChromeTabStarted) {
- customChromeTabStarted = false
+ if (externalAppStarted) {
+ externalAppStarted = false
// Workaround to detect that the Custom Chrome Tab has been closed
// If there is no coming OidcAction (that would end this Node),
// consider that the user has cancelled the login
@@ -122,9 +121,6 @@ class LoginFlowNode @AssistedInject constructor(
@Parcelize
data class CreateAccount(val url: String) : NavTarget
-
- @Parcelize
- data class OidcView(val oidcDetails: OidcDetails) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -249,9 +245,6 @@ class LoginFlowNode @AssistedInject constructor(
NavTarget.LoginPassword -> {
createNode(buildContext)
}
- is NavTarget.OidcView -> {
- oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.oidcDetails.url)
- }
is NavTarget.CreateAccount -> {
val inputs = CreateAccountNode.Inputs(
url = navTarget.url,
@@ -262,15 +255,9 @@ class LoginFlowNode @AssistedInject constructor(
}
private fun navigateToMas(oidcDetails: OidcDetails) {
- if (oidcEntryPoint.canUseCustomTab()) {
- // In this case open a Chrome Custom tab
- activity?.let {
- customChromeTabStarted = true
- oidcEntryPoint.openUrlInCustomTab(it, darkTheme, oidcDetails.url)
- }
- } else {
- // Fallback to WebView mode
- backstack.push(NavTarget.OidcView(oidcDetails))
+ activity?.let {
+ externalAppStarted = true
+ it.openUrlInChromeCustomTab(null, darkTheme, oidcDetails.url)
}
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt
index b4fff3acab..abf1327913 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt
@@ -81,6 +81,8 @@ fun LoginModeView(
LoginMode.PasswordLogin -> onNeedLoginPassword()
is LoginMode.AccountCreation -> onCreateAccountContinue(loginModeData.url)
}
+ // Also clear the data, to let the next screen be able to go back
+ onClearError()
}
AsyncData.Uninitialized -> Unit
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt
index 32d6671452..8ecfbb5628 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt
@@ -88,7 +88,7 @@ fun ConfirmAccountProviderView(
TextButton(
text = stringResource(id = R.string.screen_account_provider_change),
onClick = onChange,
- enabled = true,
+ enabled = !isLoading,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginChangeServer)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenter.kt
index ac1d637bc8..371c3c3910 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenter.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenter.kt
@@ -21,6 +21,7 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.extensions.flatMap
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@@ -74,7 +75,7 @@ class CreateAccountPresenter @AssistedInject constructor(
private fun CoroutineScope.importSession(message: String, loggedInState: MutableState>) = launch {
loggedInState.value = AsyncAction.Loading
- runCatching {
+ runCatchingExceptions {
messageParser.parse(message)
}.flatMap { externalSession ->
authenticationService.importCreatedSession(externalSession)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountStateProvider.kt
index 3618bc301c..e2e7ac3251 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountStateProvider.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountStateProvider.kt
@@ -17,7 +17,7 @@ open class CreateAccountStateProvider : PreviewParameterProvider Unit,
modifier: Modifier = Modifier,
) {
+ val autofillManager = LocalAutofillManager.current
+
+ BackHandler {
+ autofillManager?.cancel()
+ onBackClick()
+ }
+
val isLoading by remember(state.loginAction) {
derivedStateOf {
state.loginAction is AsyncData.Loading
@@ -82,6 +92,8 @@ fun LoginPasswordView(
// Clear focus to prevent keyboard issues with textfields
focusManager.clearFocus(force = true)
+ autofillManager?.commit()
+
state.eventSink(LoginPasswordEvents.Submit)
}
@@ -90,7 +102,12 @@ fun LoginPasswordView(
topBar = {
TopAppBar(
title = {},
- navigationIcon = { BackButton(onClick = onBackClick) },
+ navigationIcon = {
+ BackButton(onClick = {
+ autofillManager?.cancel()
+ onBackClick()
+ })
+ },
)
}
) { padding ->
@@ -175,14 +192,9 @@ private fun LoginForm(
.fillMaxWidth()
.onTabOrEnterKeyFocusNext(focusManager)
.testTag(TestTags.loginEmailUsername)
- .autofill(
- autofillTypes = listOf(AutofillType.Username),
- onFill = {
- val sanitized = it.sanitize()
- loginFieldState = sanitized
- eventSink(LoginPasswordEvents.SetLogin(sanitized))
- }
- ),
+ .semantics {
+ contentType = ContentType.Username
+ },
placeholder = stringResource(CommonStrings.common_username),
onValueChange = {
val sanitized = it.sanitize()
@@ -227,14 +239,9 @@ private fun LoginForm(
.fillMaxWidth()
.onTabOrEnterKeyFocusNext(focusManager)
.testTag(TestTags.loginPassword)
- .autofill(
- autofillTypes = listOf(AutofillType.Password),
- onFill = {
- val sanitized = it.sanitize()
- passwordFieldState = sanitized
- eventSink(LoginPasswordEvents.SetPassword(sanitized))
- }
- ),
+ .semantics {
+ contentType = ContentType.Password
+ },
onValueChange = {
val sanitized = it.sanitize()
passwordFieldState = sanitized
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/ErrorFormatterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/ErrorFormatterTest.kt
index 94155c6ac6..2741bbd62a 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/ErrorFormatterTest.kt
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/ErrorFormatterTest.kt
@@ -17,7 +17,7 @@ class ErrorFormatterTest {
// region loginError
@Test
fun `loginError - invalid unknown error returns unknown error message`() {
- val error = Throwable("Some unknown error")
+ val error = RuntimeException("Some unknown error")
assertThat(loginError(error)).isEqualTo(CommonStrings.error_unknown)
}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt
index 98a185cac4..5bad8a3638 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt
@@ -17,7 +17,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_3
-import io.element.android.libraries.matrix.test.A_THROWABLE
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
@@ -113,7 +113,7 @@ class ChooseAccountProviderPresenterTest {
}
awaitItem().also {
assertThat(it.selectedAccountProvider).isEqualTo(accountProvider1)
- authenticationService.givenChangeServerError(A_THROWABLE)
+ authenticationService.givenChangeServerError(AN_EXCEPTION)
it.eventSink(ChooseAccountProviderEvents.Continue)
skipItems(1) // Loading
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt
index a2384baaeb..cbf61f4678 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt
@@ -22,9 +22,9 @@ import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticat
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
-import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
@@ -118,7 +118,7 @@ class ConfirmAccountProviderPresenterTest {
assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
- authenticationService.givenOidcCancelError(A_THROWABLE)
+ authenticationService.givenOidcCancelError(AN_EXCEPTION)
defaultOidcActionFlow.post(OidcAction.GoBack)
val cancelFailureState = awaitItem()
assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
@@ -173,7 +173,7 @@ class ConfirmAccountProviderPresenterTest {
assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
- authenticationService.givenLoginError(A_THROWABLE)
+ authenticationService.givenLoginError(AN_EXCEPTION)
defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
val cancelLoadingState = awaitItem()
assertThat(cancelLoadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
@@ -225,7 +225,7 @@ class ConfirmAccountProviderPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
- authenticationService.givenChangeServerError(Throwable())
+ authenticationService.givenChangeServerError(RuntimeException())
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading
val failureState = awaitItem()
@@ -246,7 +246,7 @@ class ConfirmAccountProviderPresenterTest {
val initialState = awaitItem()
// Submit will return an error
- authenticationService.givenChangeServerError(A_THROWABLE)
+ authenticationService.givenChangeServerError(AN_EXCEPTION)
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenterTest.kt
index 6f95cf199b..d97a021fcc 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenterTest.kt
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenterTest.kt
@@ -16,11 +16,14 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
+import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_SESSION_ID
+import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.core.aBuildMeta
+import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@@ -89,12 +92,16 @@ class CreateAccountPresenterTest {
defaultLoginUserStory.setLoginFlowIsDone(false)
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
val lambda = lambdaRecorder { _ -> anExternalSession() }
+ val sessionVerificationService = FakeSessionVerificationService()
+ val client = FakeMatrixClient(sessionVerificationService = sessionVerificationService)
+ val clientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) })
val presenter = createPresenter(
authenticationService = FakeMatrixAuthenticationService(
importCreatedSessionLambda = { Result.success(A_SESSION_ID) }
),
messageParser = FakeMessageParser(lambda),
defaultLoginUserStory = defaultLoginUserStory,
+ clientProvider = clientProvider,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -102,6 +109,7 @@ class CreateAccountPresenterTest {
val initialState = awaitItem()
initialState.eventSink(CreateAccountEvents.OnMessageReceived("aMessage"))
assertThat(awaitItem().createAction.isLoading()).isTrue()
+ sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
assertThat(awaitItem().createAction.dataOrNull()).isEqualTo(A_SESSION_ID)
}
lambda.assertions().isCalledOnce().with(value("aMessage"))
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt
index 8e2fa4888c..2413e09243 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt
@@ -14,10 +14,10 @@ import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_PASSWORD
import io.element.android.libraries.matrix.test.A_SESSION_ID
-import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.tests.testutils.WarmUpRule
@@ -96,12 +96,12 @@ class LoginPasswordPresenterTest {
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
skipItems(1)
val loginAndPasswordState = awaitItem()
- authenticationService.givenLoginError(A_THROWABLE)
+ authenticationService.givenLoginError(AN_EXCEPTION)
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
val submitState = awaitItem()
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
val loggedInState = awaitItem()
- assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Failure(A_THROWABLE))
+ assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Failure(AN_EXCEPTION))
}
}
@@ -117,13 +117,13 @@ class LoginPasswordPresenterTest {
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
skipItems(1)
val loginAndPasswordState = awaitItem()
- authenticationService.givenLoginError(A_THROWABLE)
+ authenticationService.givenLoginError(AN_EXCEPTION)
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
val submitState = awaitItem()
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
val loggedInState = awaitItem()
// Check an error was returned
- assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Failure(A_THROWABLE))
+ assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Failure(AN_EXCEPTION))
// Assert the error is then cleared
loggedInState.eventSink(LoginPasswordEvents.ClearError)
val clearedState = awaitItem()
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt
index 45d7bc53e5..99deb8b7bb 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt
@@ -8,17 +8,21 @@
package io.element.android.features.login.impl.screens.loginpassword
import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.matrix.test.A_PASSWORD
import io.element.android.libraries.matrix.test.A_USER_NAME
+import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
@@ -120,15 +124,15 @@ class LoginPasswordViewTest {
eventSink = eventsRecorder,
),
)
- rule.onNodeWithText(A_PASSWORD).assertDoesNotExist()
+ rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••"))
// Show password
val a11yShowPassword = rule.activity.getString(CommonStrings.a11y_show_password)
rule.onNodeWithContentDescription(a11yShowPassword).performClick()
- rule.onNodeWithText(A_PASSWORD).assertExists()
+ rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText(A_PASSWORD))
// Hide password
val a11yHidePassword = rule.activity.getString(CommonStrings.a11y_hide_password)
rule.onNodeWithContentDescription(a11yHidePassword).performClick()
- rule.onNodeWithText(A_PASSWORD).assertDoesNotExist()
+ rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••"))
}
@Test
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 d57c67df7e..7749d1502b 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
@@ -24,9 +24,9 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_3
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_LOGIN_HINT
-import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.oidc.api.OidcActionFlow
@@ -192,7 +192,7 @@ class OnBoardingPresenterTest {
skipItems(3)
awaitItem().also {
assertThat(it.defaultAccountProvider).isEqualTo(A_HOMESERVER_URL)
- authenticationService.givenChangeServerError(A_THROWABLE)
+ authenticationService.givenChangeServerError(AN_EXCEPTION)
it.eventSink(OnBoardingEvents.OnSignIn(A_HOMESERVER_URL))
skipItems(1) // Loading
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt
index 52af14cfe4..8ac42b4c93 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt
@@ -14,6 +14,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.login.impl.R
+import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.test.AN_EXCEPTION
@@ -36,9 +37,13 @@ class OnboardingViewTest {
@Test
fun `when can create account - clicking on create account calls the expected callback`() {
+ val eventSink = EventsRecorder(expectEvents = false)
ensureCalledOnce { callback ->
rule.setOnboardingView(
- state = anOnBoardingState(canCreateAccount = true),
+ state = anOnBoardingState(
+ canCreateAccount = true,
+ eventSink = eventSink,
+ ),
onCreateAccount = callback,
)
rule.clickOn(R.string.screen_onboarding_sign_up)
@@ -47,9 +52,13 @@ class OnboardingViewTest {
@Test
fun `when can login with QR code - clicking on sign in with QR code calls the expected callback`() {
+ val eventSink = EventsRecorder(expectEvents = false)
ensureCalledOnce { callback ->
rule.setOnboardingView(
- state = anOnBoardingState(canLoginWithQrCode = true),
+ state = anOnBoardingState(
+ canLoginWithQrCode = true,
+ eventSink = eventSink,
+ ),
onSignInWithQrCode = callback,
)
rule.clickOn(R.string.screen_onboarding_sign_in_with_qr_code)
@@ -73,11 +82,13 @@ class OnboardingViewTest {
private fun `when can login with QR code - clicking on sign in manually calls the expected callback`(
mustChooseAccountProvider: Boolean,
) {
+ val eventSink = EventsRecorder(expectEvents = false)
ensureCalledOnceWithParam(mustChooseAccountProvider) { callback ->
rule.setOnboardingView(
state = anOnBoardingState(
canLoginWithQrCode = true,
mustChooseAccountProvider = mustChooseAccountProvider,
+ eventSink = eventSink,
),
onSignIn = callback,
)
@@ -102,12 +113,14 @@ class OnboardingViewTest {
private fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback`(
mustChooseAccountProvider: Boolean,
) {
+ val eventSink = EventsRecorder(expectEvents = false)
ensureCalledOnceWithParam(mustChooseAccountProvider) { callback ->
rule.setOnboardingView(
state = anOnBoardingState(
canLoginWithQrCode = false,
canCreateAccount = false,
mustChooseAccountProvider = mustChooseAccountProvider,
+ eventSink = eventSink,
),
onSignIn = callback,
)
@@ -145,10 +158,12 @@ class OnboardingViewTest {
@Test
fun `clicking on report a problem calls the sign in callback`() {
+ val eventSink = EventsRecorder(expectEvents = false)
ensureCalledOnce { callback ->
rule.setOnboardingView(
state = anOnBoardingState(
canReportBug = true,
+ eventSink = eventSink,
),
onReportProblem = callback,
)
@@ -160,15 +175,64 @@ class OnboardingViewTest {
@Test
fun `cannot report a problem when the feature is disabled`() {
+ val eventSink = EventsRecorder(expectEvents = false)
rule.setOnboardingView(
state = anOnBoardingState(
canReportBug = false,
+ eventSink = eventSink,
),
)
val text = rule.activity.getString(CommonStrings.common_report_a_problem)
rule.onNodeWithText(text).assertDoesNotExist()
}
+ @Test
+ fun `when success PasswordLogin - the expected callback is invoked and the event is received`() {
+ val eventSink = EventsRecorder()
+ ensureCalledOnce { callback ->
+ rule.setOnboardingView(
+ state = anOnBoardingState(
+ loginMode = AsyncData.Success(LoginMode.PasswordLogin),
+ eventSink = eventSink,
+ ),
+ onNeedLoginPassword = callback,
+ )
+ }
+ eventSink.assertSingle(OnBoardingEvents.ClearError)
+ }
+
+ @Test
+ fun `when success Oidc - the expected callback is invoked and the event is received`() {
+ val eventSink = EventsRecorder()
+ val oidcDetails = OidcDetails("aUrl")
+ ensureCalledOnceWithParam(oidcDetails) { callback ->
+ rule.setOnboardingView(
+ state = anOnBoardingState(
+ loginMode = AsyncData.Success(LoginMode.Oidc(oidcDetails)),
+ eventSink = eventSink,
+ ),
+ onOidcDetails = callback,
+ )
+ }
+ eventSink.assertSingle(OnBoardingEvents.ClearError)
+ }
+
+ @Test
+ fun `when success AccountCreation - the expected callback is invoked and the event is received`() {
+ val eventSink = EventsRecorder()
+ val oidcDetails = OidcDetails("aUrl")
+ ensureCalledOnceWithParam(oidcDetails.url) { callback ->
+ rule.setOnboardingView(
+ state = anOnBoardingState(
+ loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")),
+ eventSink = eventSink,
+ ),
+ onCreateAccountContinue = callback,
+ )
+ }
+ eventSink.assertSingle(OnBoardingEvents.ClearError)
+ }
+
private fun AndroidComposeTestRule.setOnboardingView(
state: OnBoardingState,
onSignInWithQrCode: () -> Unit = EnsureNeverCalled(),
diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt
index 61a1f1371d..3a309ab7b9 100644
--- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt
+++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt
@@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
-import io.element.android.libraries.matrix.test.A_THROWABLE
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.tests.testutils.WarmUpRule
@@ -165,7 +165,7 @@ class LogoutPresenterTest {
fun `present - logout with error then cancel`() = runTest {
val matrixClient = FakeMatrixClient().apply {
logoutLambda = { _, _ ->
- throw A_THROWABLE
+ throw AN_EXCEPTION
}
}
val presenter = createLogoutPresenter(
@@ -182,7 +182,7 @@ class LogoutPresenterTest {
val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
val errorState = awaitItem()
- assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE))
+ assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
errorState.eventSink.invoke(LogoutEvents.CloseDialogs)
val finalState = awaitItem()
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
@@ -194,9 +194,7 @@ class LogoutPresenterTest {
val matrixClient = FakeMatrixClient().apply {
logoutLambda = { ignoreSdkError, _ ->
if (!ignoreSdkError) {
- throw A_THROWABLE
- } else {
- null
+ throw AN_EXCEPTION
}
}
}
@@ -214,7 +212,7 @@ class LogoutPresenterTest {
val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
val errorState = awaitItem()
- assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE))
+ assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
errorState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = true))
val loadingState2 = awaitItem()
assertThat(loadingState2.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt
index 158ce0c0ea..d5dff82c5c 100644
--- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt
+++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt
@@ -17,7 +17,7 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
-import io.element.android.libraries.matrix.test.A_THROWABLE
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.tests.testutils.WarmUpRule
@@ -117,7 +117,7 @@ class DirectLogoutPresenterTest {
fun `present - logout with error then cancel`() = runTest {
val matrixClient = FakeMatrixClient().apply {
logoutLambda = { _, _ ->
- throw A_THROWABLE
+ throw AN_EXCEPTION
}
}
val presenter = createDirectLogoutPresenter(
@@ -134,7 +134,7 @@ class DirectLogoutPresenterTest {
val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
val errorState = awaitItem()
- assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE))
+ assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
errorState.eventSink.invoke(DirectLogoutEvents.CloseDialogs)
val finalState = awaitItem()
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
@@ -146,9 +146,7 @@ class DirectLogoutPresenterTest {
val matrixClient = FakeMatrixClient().apply {
logoutLambda = { ignoreSdkError, _ ->
if (!ignoreSdkError) {
- throw A_THROWABLE
- } else {
- null
+ throw AN_EXCEPTION
}
}
}
@@ -166,7 +164,7 @@ class DirectLogoutPresenterTest {
val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
val errorState = awaitItem()
- assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE))
+ assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
errorState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = true))
val loadingState2 = awaitItem()
assertThat(loadingState2.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt
index 21b8d03381..ca70ca55de 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt
@@ -13,6 +13,8 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetValue
+import androidx.compose.material3.rememberBottomSheetScaffoldState
+import androidx.compose.material3.rememberStandardBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@@ -29,10 +31,8 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.min
+import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold
-import io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomSheetState
-import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberBottomSheetScaffoldState
-import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberStandardBottomSheetState
import kotlin.math.roundToInt
/**
@@ -139,8 +139,8 @@ internal fun ExpandableBottomSheetScaffold(
modifier = Modifier.fillMaxHeight(),
measurePolicy = { measurables, constraints ->
val constraintHeight = constraints.maxHeight
- val offset = scaffoldState.bottomSheetState.getIntOffset() ?: 0
- val height = Integer.max(0, constraintHeight - offset)
+ val offset = tryOrNull { scaffoldState.bottomSheetState.requireOffset() } ?: 0f
+ val height = Integer.max(0, constraintHeight - offset.roundToInt())
val top = measurables[0].measure(
constraints.copy(
minHeight = height,
@@ -165,12 +165,6 @@ internal fun ExpandableBottomSheetScaffold(
)
}
-private fun CustomSheetState.getIntOffset(): Int? = try {
- requireOffset().roundToInt()
-} catch (e: IllegalStateException) {
- null
-}
-
private sealed interface Slot {
data class SheetContent(val key: Int?) : Slot
data object DragHandle : Slot
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 6acf48e58b..fc6d01d3a9 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
@@ -50,6 +50,7 @@ import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.di.annotations.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
@@ -69,7 +70,8 @@ import kotlinx.coroutines.launch
class MessagesNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- private val coroutineScope: CoroutineScope,
+ @SessionCoroutineScope
+ private val sessionCoroutineScope: CoroutineScope,
private val room: BaseRoom,
private val analyticsService: AnalyticsService,
messageComposerPresenterFactory: MessageComposerPresenter.Factory,
@@ -115,7 +117,7 @@ class MessagesNode @AssistedInject constructor(
super.onBuilt()
lifecycle.subscribe(
onCreate = {
- coroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) }
+ sessionCoroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) }
},
onDestroy = {
mediaPlayer.close()
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 6be5646649..74cb9799f2 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
@@ -56,6 +56,7 @@ import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@@ -387,7 +388,7 @@ class MessagesPresenter @AssistedInject constructor(
private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState>) = launch(dispatchers.io) {
inviteProgress.value = AsyncData.Loading()
- runCatching {
+ runCatchingExceptions {
val memberList = when (val memberState = room.membersStateFlow.value) {
is RoomMembersState.Ready -> memberState.roomMembers
is RoomMembersState.Error -> memberState.prevRoomMembers.orEmpty()
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 90bc3d0427..b48ff98073 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt
@@ -27,6 +27,7 @@ import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.firstInstanceOf
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
@@ -240,7 +241,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
sendActionState: MutableState,
dismissAfterSend: Boolean,
replyParameters: ReplyParameters?,
- ) = runCatching {
+ ) = runCatchingExceptions {
val context = coroutineContext
val progressCallback = object : ProgressCallback {
override fun onProgress(current: Long, total: Long) {
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt
index 7b5f07649b..1d3b9778c3 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt
@@ -15,7 +15,7 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.room.JoinedRoom
-import io.element.android.libraries.matrix.ui.room.observeRoomMemberIdentityStateChange
+import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -30,7 +30,7 @@ class IdentityChangeStatePresenter @Inject constructor(
override fun present(): IdentityChangeState {
val coroutineScope = rememberCoroutineScope()
val roomMemberIdentityStateChange by produceState(persistentListOf()) {
- observeRoomMemberIdentityStateChange(room)
+ room.roomMemberIdentityStateChange(waitForEncryption = true).collect { value = it }
}
fun handleEvent(event: IdentityChangeEvent) {
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt
index 960d54487a..013003e4b7 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt
@@ -16,6 +16,7 @@ import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
@@ -28,7 +29,8 @@ import kotlinx.coroutines.launch
class ForwardMessagesPresenter @AssistedInject constructor(
@Assisted eventId: String,
@Assisted private val timelineProvider: TimelineProvider,
- private val appCoroutineScope: CoroutineScope,
+ @SessionCoroutineScope
+ private val sessionCoroutineScope: CoroutineScope,
) : Presenter {
private val eventId: EventId = EventId(eventId)
@@ -40,7 +42,7 @@ class ForwardMessagesPresenter @AssistedInject constructor(
private val forwardingActionState: MutableState>> = mutableStateOf(AsyncAction.Uninitialized)
fun onRoomSelected(roomIds: List) {
- appCoroutineScope.forwardEvent(eventId, roomIds.toPersistentList(), forwardingActionState)
+ sessionCoroutineScope.forwardEvent(eventId, roomIds.toPersistentList(), forwardingActionState)
}
@Composable
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt
index 9172a6c5ac..b1728f4657 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt
@@ -24,7 +24,7 @@ open class ForwardMessagesStateProvider : PreviewParameterProvider
- handlePickedMedia(uri)
+ handlePickedMedia(uri, MimeTypes.OctetStream)
}
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri ->
- handlePickedMedia(uri, MimeTypes.IMAGE_JPEG)
+ handlePickedMedia(uri, MimeTypes.Jpeg)
}
val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker { uri ->
- handlePickedMedia(uri, MimeTypes.VIDEO_MP4)
+ handlePickedMedia(uri, MimeTypes.Mp4)
}
val isFullScreen = rememberSaveable {
mutableStateOf(false)
@@ -199,7 +202,7 @@ class MessageComposerPresenter @AssistedInject constructor(
DisposableEffect(Unit) {
// Declare that the user is not typing anymore when the composer is disposed
onDispose {
- appCoroutineScope.launch {
+ sessionCoroutineScope.launch {
if (sendTypingNotifications) {
room.typingNotice(false)
}
@@ -235,12 +238,12 @@ class MessageComposerPresenter @AssistedInject constructor(
}
}
is MessageComposerEvents.SendMessage -> {
- appCoroutineScope.sendMessage(
+ sessionCoroutineScope.sendMessage(
markdownTextEditorState = markdownTextEditorState,
richTextEditorState = richTextEditorState,
)
}
- is MessageComposerEvents.SendUri -> appCoroutineScope.sendAttachment(
+ is MessageComposerEvents.SendUri -> sessionCoroutineScope.sendAttachment(
attachment = Attachment.Media(
localMedia = localMediaFactory.createFromUri(
uri = event.uri,
@@ -337,7 +340,7 @@ class MessageComposerPresenter @AssistedInject constructor(
}
MessageComposerEvents.SaveDraft -> {
val draft = createDraftFromState(markdownTextEditorState, richTextEditorState)
- appCoroutineScope.updateDraft(draft, isVolatile = false)
+ sessionCoroutineScope.updateDraft(draft, isVolatile = false)
}
}
}
@@ -512,7 +515,7 @@ class MessageComposerPresenter @AssistedInject constructor(
private suspend fun sendMedia(
uri: Uri,
mimeType: String,
- ) = runCatching {
+ ) = runCatchingExceptions {
mediaSender.sendMedia(
uri = uri,
mimeType = mimeType,
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 82f98724f1..a7901ff52d 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
@@ -38,6 +38,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.powerlevels.canPinUnpin
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
@@ -67,7 +68,8 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
private val linkPresenter: Presenter,
private val snackbarDispatcher: SnackbarDispatcher,
@Assisted private val actionListPresenter: Presenter,
- private val appCoroutineScope: CoroutineScope,
+ @SessionCoroutineScope
+ private val sessionCoroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService,
) : Presenter {
@AssistedFactory
@@ -123,7 +125,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
fun handleEvents(event: PinnedMessagesListEvents) {
when (event) {
- is PinnedMessagesListEvents.HandleAction -> appCoroutineScope.handleTimelineAction(event.action, event.event)
+ is PinnedMessagesListEvents.HandleAction -> sessionCoroutineScope.handleTimelineAction(event.action, event.event)
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt
index e1127d2644..cb3b553497 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt
@@ -17,7 +17,7 @@ open class ReportMessageStateProvider : PreviewParameterProvider
+ room.markAsRead(receiptType = ReceiptType.FULLY_READ)
+ .onFailure {
+ Timber.e("Failed to mark room $roomId as fully read", it)
+ }
+ }
+ }
+ }
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
index 3be27fe7d0..4684b8cfdd 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
@@ -8,6 +8,7 @@
package io.element.android.features.messages.impl.timeline
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
@@ -37,6 +38,7 @@ import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.room.JoinedRoom
@@ -65,7 +67,8 @@ class TimelinePresenter @AssistedInject constructor(
timelineItemsFactoryCreator: TimelineItemsFactory.Creator,
private val room: JoinedRoom,
private val dispatchers: CoroutineDispatchers,
- private val appScope: CoroutineScope,
+ @SessionCoroutineScope
+ private val sessionCoroutineScope: CoroutineScope,
@Assisted private val navigator: MessagesNavigator,
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
private val sendPollResponseAction: SendPollResponseAction,
@@ -76,6 +79,7 @@ class TimelinePresenter @AssistedInject constructor(
private val resolveVerifiedUserSendFailurePresenter: Presenter,
private val typingNotificationPresenter: Presenter,
private val roomCallStatePresenter: Presenter,
+ private val markAsFullyRead: MarkAsFullyRead,
) : Presenter {
@AssistedFactory
interface Factory {
@@ -133,7 +137,7 @@ class TimelinePresenter @AssistedInject constructor(
newEventState.value = NewEventState.None
}
Timber.d("## sendReadReceiptIfNeeded firstVisibleIndex: ${event.firstIndex}")
- appScope.sendReadReceiptIfNeeded(
+ sessionCoroutineScope.sendReadReceiptIfNeeded(
firstVisibleIndex = event.firstIndex,
timelineItems = timelineItems,
lastReadReceiptId = lastReadReceiptId,
@@ -143,13 +147,13 @@ class TimelinePresenter @AssistedInject constructor(
newEventState.value = NewEventState.None
}
}
- is TimelineEvents.SelectPollAnswer -> appScope.launch {
+ is TimelineEvents.SelectPollAnswer -> sessionCoroutineScope.launch {
sendPollResponseAction.execute(
pollStartId = event.pollStartId,
answerId = event.answerId
)
}
- is TimelineEvents.EndPoll -> appScope.launch {
+ is TimelineEvents.EndPoll -> sessionCoroutineScope.launch {
endPollAction.execute(
pollStartId = event.pollStartId,
)
@@ -177,6 +181,12 @@ class TimelinePresenter @AssistedInject constructor(
}
}
+ DisposableEffect(Unit) {
+ onDispose {
+ markAsFullyRead(room.roomId)
+ }
+ }
+
LaunchedEffect(Unit) {
timelineItemsFactory.timelineItems
.onEach { newTimelineItems ->
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt
index ad5337031c..2b48a99dcd 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt
@@ -47,6 +47,7 @@ import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.messageFromMeBackground
import io.element.android.libraries.designsystem.theme.messageFromOtherBackground
+import io.element.android.libraries.designsystem.utils.LocalUiTestMode
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.utils.time.isTalkbackActive
@@ -112,7 +113,9 @@ fun MessageEventBubble(
state.isMine -> ElementTheme.colors.messageFromMeBackground
else -> ElementTheme.colors.messageFromOtherBackground
}
- val bubbleShape = bubbleShape()
+ // If we're running in UI test mode, we want to use a different shape to avoid
+ // this issue: https://issuetracker.google.com/issues/366255137
+ val bubbleShape = if (LocalUiTestMode.current) RoundedCornerShape(12.dp) else bubbleShape()
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt
index a29c13ccd5..644cbf2310 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt
@@ -18,7 +18,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.invisibleToUser
+import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -80,7 +80,7 @@ fun TimelineEventTimestampView(
.clickable(isVerifiedUserSendFailure) {
eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event))
}
- .semantics { invisibleToUser() }
+ .semantics { hideFromAccessibility() }
)
}
@@ -95,7 +95,7 @@ fun TimelineEventTimestampView(
.clickable {
eventSink(TimelineEvents.ShowShieldDialog(shield))
}
- .semantics { invisibleToUser() },
+ .semantics { hideFromAccessibility() },
tint = shield.toIconColor(),
)
Spacer(modifier = Modifier.width(4.dp))
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
index 1c9e8d5c2f..55ee0d7a4f 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
@@ -39,7 +39,7 @@ import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
-import androidx.compose.ui.semantics.invisibleToUser
+import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
@@ -439,7 +439,7 @@ private fun MessageSenderInformation(
// Add external clickable modifier with no indicator so the touch target is larger than just the display name
.clickable(onClick = onClick, enabled = true, interactionSource = remember { MutableInteractionSource() }, indication = null)
.clearAndSetSemantics {
- invisibleToUser()
+ hideFromAccessibility()
}
) {
Avatar(
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt
index 02cfa39830..07811b532d 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt
@@ -16,7 +16,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.invisibleToUser
+import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
@@ -43,7 +43,7 @@ fun TimelineItemReactionsView(
var expanded: Boolean by rememberSaveable { mutableStateOf(false) }
TimelineItemReactionsView(
modifier = modifier.semantics {
- invisibleToUser()
+ hideFromAccessibility()
},
reactions = reactionsState.reactions,
userCanSendReaction = userCanSendReaction,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt
index 680489d3bb..d419197146 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt
@@ -102,7 +102,7 @@ fun TimelineItemImageView(
}
),
model = content.thumbnailMediaRequestData,
- contentScale = ContentScale.Fit,
+ contentScale = ContentScale.Crop,
alignment = Alignment.Center,
contentDescription = description,
onState = { isLoaded = it is AsyncImagePainter.State.Success },
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt
index 0f80673aae..daabcba4d5 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt
@@ -76,7 +76,7 @@ fun TimelineItemStickerView(
mimeType = content.mimeType,
),
),
- contentScale = ContentScale.Fit,
+ contentScale = ContentScale.Crop,
alignment = Alignment.Center,
contentDescription = description,
onState = { isLoaded = it is AsyncImagePainter.State.Success },
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt
index 4ac09f106d..edd37d0b5c 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt
@@ -36,7 +36,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.invisibleToUser
+import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -122,7 +122,7 @@ fun TimelineItemVideoView(
height = content.thumbnailHeight?.toLong() ?: MAX_THUMBNAIL_HEIGHT,
)
),
- contentScale = ContentScale.Fit,
+ contentScale = ContentScale.Crop,
alignment = Alignment.Center,
contentDescription = description,
onState = { isLoaded = it is AsyncImagePainter.State.Success },
@@ -136,7 +136,7 @@ fun TimelineItemVideoView(
imageVector = CompoundIcons.PlaySolid(),
contentDescription = stringResource(id = CommonStrings.a11y_play),
colorFilter = ColorFilter.tint(Color.White),
- modifier = Modifier.semantics { invisibleToUser() }
+ modifier = Modifier.semantics { hideFromAccessibility() }
)
}
}
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 e7d80a9d6c..8493bad765 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
@@ -26,7 +26,7 @@ import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.invisibleToUser
+import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -59,7 +59,7 @@ fun TimelineItemReadReceiptView(
if (renderReadReceipts) {
ReadReceiptsRow(
modifier = modifier.clearAndSetSemantics {
- invisibleToUser()
+ hideFromAccessibility()
}
) {
ReadReceiptsAvatars(
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt
index 6f0fa9735a..57b18817f5 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt
@@ -8,6 +8,7 @@
package io.element.android.features.messages.impl.voicemessages.composer
import io.element.android.libraries.core.mimetype.MimeTypes
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.mediaplayer.api.MediaPlayer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -26,11 +27,12 @@ import javax.inject.Inject
* A media player for the voice message composer.
*
* @param mediaPlayer The [MediaPlayer] to use.
- * @param coroutineScope
+ * @param sessionCoroutineScope
*/
class VoiceMessageComposerPlayer @Inject constructor(
private val mediaPlayer: MediaPlayer,
- private val coroutineScope: CoroutineScope,
+ @SessionCoroutineScope
+ private val sessionCoroutineScope: CoroutineScope,
) {
companion object {
const val MIME_TYPE = MimeTypes.Ogg
@@ -116,7 +118,7 @@ class VoiceMessageComposerPlayer @Inject constructor(
seekJob?.cancelAndJoin()
seekingTo.value = position
- seekJob = coroutineScope.launch {
+ seekJob = sessionCoroutineScope.launch {
val mediaState = mediaPlayer.ensureMediaReady(mediaPath)
val duration = mediaState.duration ?: return@launch
val positionMs = (duration * position).toLong()
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt
index 032e00cd36..9b723ff72a 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt
@@ -22,6 +22,7 @@ import androidx.lifecycle.Lifecycle
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
@@ -44,7 +45,8 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
class VoiceMessageComposerPresenter @Inject constructor(
- private val appCoroutineScope: CoroutineScope,
+ @SessionCoroutineScope
+ private val sessionCoroutineScope: CoroutineScope,
private val voiceRecorder: VoiceRecorder,
private val analyticsService: AnalyticsService,
private val mediaSender: MediaSender,
@@ -74,11 +76,11 @@ class VoiceMessageComposerPresenter @Inject constructor(
val onLifecycleEvent = { event: Lifecycle.Event ->
when (event) {
Lifecycle.Event.ON_PAUSE -> {
- appCoroutineScope.finishRecording()
+ sessionCoroutineScope.finishRecording()
player.pause()
}
Lifecycle.Event.ON_DESTROY -> {
- appCoroutineScope.cancelRecording()
+ sessionCoroutineScope.cancelRecording()
}
else -> {}
}
@@ -145,7 +147,7 @@ class VoiceMessageComposerPresenter @Inject constructor(
isSending = true
player.pause()
analyticsService.captureComposerEvent()
- appCoroutineScope.launch {
+ sessionCoroutineScope.launch {
val result = sendMessage(
file = finishedState.file,
mimeType = finishedState.mimeType,
diff --git a/features/messages/impl/src/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 070ff442f9..e42941da0d 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
@@ -62,11 +62,11 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransa
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.AN_EVENT_ID
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_CAPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
-import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.core.aBuildMeta
@@ -139,10 +139,10 @@ class MessagesPresenterTest {
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
canUserPinUnpinResult = { Result.success(true) },
+ markAsReadResult = { lambdaError() }
),
typingNoticeResult = { Result.success(Unit) },
)
- assertThat(room.baseRoom.markAsReadCalls).isEmpty()
val presenter = createMessagesPresenter(joinedRoom = room)
presenter.testWithLifecycleOwner {
runCurrent()
@@ -744,7 +744,7 @@ class MessagesPresenterTest {
canUserPinUnpinResult = { Result.success(true) },
),
typingNoticeResult = { Result.success(Unit) },
- inviteUserResult = { Result.failure(Throwable("Oops!")) },
+ inviteUserResult = { Result.failure(RuntimeException("Oops!")) },
)
room.givenRoomMembersState(
RoomMembersState.Ready(
@@ -848,12 +848,12 @@ class MessagesPresenterTest {
fun `present - permission to redact other`() = runTest {
val joinedRoom = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
- canRedactOtherResult = { Result.success(true) },
- canUserSendMessageResult = { _, _ -> Result.success(true) },
- canRedactOwnResult = { Result.success(false) },
- canUserJoinCallResult = { Result.success(true) },
- canUserPinUnpinResult = { Result.success(true) },
- ),
+ canRedactOtherResult = { Result.success(true) },
+ canUserSendMessageResult = { _, _ -> Result.success(true) },
+ canRedactOwnResult = { Result.success(false) },
+ canUserJoinCallResult = { Result.success(true) },
+ canUserPinUnpinResult = { Result.success(true) },
+ ),
typingNoticeResult = { Result.success(Unit) },
)
val presenter = createMessagesPresenter(joinedRoom = joinedRoom)
@@ -892,17 +892,17 @@ class MessagesPresenterTest {
@Test
fun `present - handle action pin`() = runTest {
val successPinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) }
- val failurePinEventLambda = lambdaRecorder { _: EventId -> Result.failure(A_THROWABLE) }
+ val failurePinEventLambda = lambdaRecorder { _: EventId -> Result.failure(AN_EXCEPTION) }
val analyticsService = FakeAnalyticsService()
val timeline = FakeTimeline()
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
- canUserSendMessageResult = { _, _ -> Result.success(true) },
- canRedactOwnResult = { Result.success(true) },
- canRedactOtherResult = { Result.success(true) },
- canUserJoinCallResult = { Result.success(true) },
- canUserPinUnpinResult = { Result.success(true) },
- ),
+ canUserSendMessageResult = { _, _ -> Result.success(true) },
+ canRedactOwnResult = { Result.success(true) },
+ canRedactOtherResult = { Result.success(true) },
+ canUserJoinCallResult = { Result.success(true) },
+ canUserPinUnpinResult = { Result.success(true) },
+ ),
liveTimeline = timeline,
typingNoticeResult = { Result.success(Unit) },
)
@@ -932,17 +932,17 @@ class MessagesPresenterTest {
@Test
fun `present - handle action unpin`() = runTest {
val successUnpinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) }
- val failureUnpinEventLambda = lambdaRecorder { _: EventId -> Result.failure(A_THROWABLE) }
+ val failureUnpinEventLambda = lambdaRecorder { _: EventId -> Result.failure(AN_EXCEPTION) }
val timeline = FakeTimeline()
val analyticsService = FakeAnalyticsService()
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
- canUserSendMessageResult = { _, _ -> Result.success(true) },
- canRedactOwnResult = { Result.success(true) },
- canRedactOtherResult = { Result.success(true) },
- canUserJoinCallResult = { Result.success(true) },
- canUserPinUnpinResult = { Result.success(true) },
- ),
+ canUserSendMessageResult = { _, _ -> Result.success(true) },
+ canRedactOwnResult = { Result.success(true) },
+ canRedactOtherResult = { Result.success(true) },
+ canUserJoinCallResult = { Result.success(true) },
+ canUserPinUnpinResult = { Result.success(true) },
+ ),
liveTimeline = timeline,
typingNoticeResult = { Result.success(Unit) },
)
@@ -1096,12 +1096,12 @@ class MessagesPresenterTest {
}
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
- canUserSendMessageResult = { _, _ -> Result.success(true) },
- canRedactOwnResult = { Result.success(true) },
- canRedactOtherResult = { Result.success(true) },
- canUserJoinCallResult = { Result.success(true) },
- canUserPinUnpinResult = { Result.success(true) },
- ),
+ canUserSendMessageResult = { _, _ -> Result.success(true) },
+ canRedactOwnResult = { Result.success(true) },
+ canRedactOtherResult = { Result.success(true) },
+ canUserJoinCallResult = { Result.success(true) },
+ canUserPinUnpinResult = { Result.success(true) },
+ ),
liveTimeline = timeline,
typingNoticeResult = { Result.success(Unit) },
)
@@ -1134,16 +1134,16 @@ class MessagesPresenterTest {
fun `present - when room is encrypted and a DM, the DM user's identity state is fetched onResume`() = runTest {
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
- sessionId = A_SESSION_ID,
- canUserSendMessageResult = { _, _ -> Result.success(true) },
- canRedactOwnResult = { Result.success(true) },
- canRedactOtherResult = { Result.success(true) },
- canUserJoinCallResult = { Result.success(true) },
- canUserPinUnpinResult = { Result.success(true) },
- initialRoomInfo = aRoomInfo(isDirect = true, isEncrypted = true)
- ).apply {
- givenRoomMembersState(RoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2))))
- },
+ sessionId = A_SESSION_ID,
+ canUserSendMessageResult = { _, _ -> Result.success(true) },
+ canRedactOwnResult = { Result.success(true) },
+ canRedactOtherResult = { Result.success(true) },
+ canUserJoinCallResult = { Result.success(true) },
+ canUserPinUnpinResult = { Result.success(true) },
+ initialRoomInfo = aRoomInfo(isDirect = true, isEncrypted = true)
+ ).apply {
+ givenRoomMembersState(RoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2))))
+ },
typingNoticeResult = { Result.success(Unit) },
)
val encryptionService = FakeEncryptionService(getUserIdentityResult = { Result.success(IdentityState.Verified) })
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt
index 9d378f7de7..4f6ac91890 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt
@@ -68,6 +68,7 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
+import io.element.android.tests.testutils.setSafeContent
import kotlinx.collections.immutable.persistentListOf
import org.junit.Rule
import org.junit.Test
@@ -567,11 +568,9 @@ private fun AndroidComposeTestRule.setMessa
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
) {
- setContent {
+ setSafeContent {
// Cannot use the RichTextEditor, so simulate a LocalInspectionMode
- CompositionLocalProvider(
- LocalInspectionMode provides true
- ) {
+ CompositionLocalProvider(LocalInspectionMode provides true) {
MessagesView(
state = state,
onBackClick = onBackClick,
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt
index a8190e8776..6a83437758 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt
@@ -14,6 +14,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.setSafeContent
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
@@ -54,7 +55,7 @@ class ResolveVerifiedUserSendFailureViewTest {
private fun AndroidComposeTestRule.setResolveVerifiedUserSendFailureView(
state: ResolveVerifiedUserSendFailureState,
) {
- setContent {
+ setSafeContent {
ResolveVerifiedUserSendFailureView(state = state)
}
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt
index 1b4caa8bdd..757e682592 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt
@@ -97,6 +97,6 @@ class ForwardMessagesPresenterTest {
) = ForwardMessagesPresenter(
eventId = eventId.value,
timelineProvider = LiveTimelineProvider(fakeRoom),
- appCoroutineScope = this,
+ sessionCoroutineScope = this,
)
}
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 5e669ca027..f3d815d563 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
@@ -1539,7 +1539,7 @@ class MessageComposerPresenterTest {
draftService: ComposerDraftService = FakeComposerDraftService(),
) = MessageComposerPresenter(
navigator = navigator,
- appCoroutineScope = this,
+ sessionCoroutineScope = this,
room = room,
mediaPickerProvider = pickerProvider,
featureFlagService = featureFlagService,
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 985e9f055f..535b11c522 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
@@ -25,7 +25,7 @@ import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.test.AN_EVENT_ID
-import io.element.android.libraries.matrix.test.A_THROWABLE
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
@@ -157,7 +157,7 @@ class PinnedMessagesListPresenterTest {
@Test
fun `present - unpin event`() = runTest {
val successUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.success(true) }
- val failureUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.failure(A_THROWABLE) }
+ val failureUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.failure(AN_EXCEPTION) }
val pinnedEventsTimeline = createPinnedMessagesTimeline()
val analyticsService = FakeAnalyticsService()
val room = FakeJoinedRoom(
@@ -337,7 +337,7 @@ class PinnedMessagesListPresenterTest {
actionListPresenter = { anActionListState() },
linkPresenter = { aLinkState() },
analyticsService = analyticsService,
- appCoroutineScope = this,
+ sessionCoroutineScope = this,
)
}
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt
index 4d66365a58..5e9ad97f87 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt
@@ -103,7 +103,7 @@ private fun AndroidComposeTestRule.setPinne
onLinkClick: (Link) -> Unit = EnsureNeverCalledWithParam(),
onLinkLongClick: (Link) -> Unit = EnsureNeverCalledWithParam(),
) {
- setSafeContent {
+ setSafeContent(clearAndroidUiDispatcher = true) {
PinnedMessagesListView(
state = state,
onBackClick = onBackClick,
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt
index a5f3417d5d..d2d5914674 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt
@@ -12,6 +12,7 @@ import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.createComposeRule
import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.utils.FakeMentionSpanFormatter
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
@@ -35,7 +36,7 @@ class DefaultHtmlConverterProviderTest {
@Test
fun `calling provide without calling Update first should throw an exception`() {
- val exception = runCatching { provider.provide() }.exceptionOrNull()
+ val exception = runCatchingExceptions { provider.provide() }.exceptionOrNull()
assertThat(exception).isInstanceOf(IllegalStateException::class.java)
}
@@ -47,7 +48,7 @@ class DefaultHtmlConverterProviderTest {
provider.Update()
}
}
- val htmlConverter = runCatching { provider.provide() }.getOrNull()
+ val htmlConverter = runCatchingExceptions { provider.provide() }.getOrNull()
assertThat(htmlConverter).isNotNull()
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultMarkAsFullyReadTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultMarkAsFullyReadTest.kt
new file mode 100644
index 0000000000..6340ce56a5
--- /dev/null
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultMarkAsFullyReadTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package io.element.android.features.messages.impl.timeline
+
+import io.element.android.libraries.matrix.api.timeline.ReceiptType
+import io.element.android.libraries.matrix.test.A_ROOM_ID
+import io.element.android.libraries.matrix.test.FakeMatrixClient
+import io.element.android.libraries.matrix.test.room.FakeBaseRoom
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class DefaultMarkAsFullyReadTest {
+ @Test
+ fun `When room is not found, then no exception is thrown`() = runTest {
+ val markAsFullyRead = DefaultMarkAsFullyRead(
+ FakeMatrixClient(
+ sessionCoroutineScope = backgroundScope,
+ ).apply {
+ givenGetRoomResult(A_ROOM_ID, null)
+ }
+ )
+ markAsFullyRead.invoke(A_ROOM_ID)
+ runCurrent()
+ }
+
+ @Test
+ fun `When room is found, the expected method is invoked`() = runTest {
+ val markAsReadResult = lambdaRecorder> { Result.success(Unit) }
+ val baseRoom = FakeBaseRoom(
+ markAsReadResult = markAsReadResult
+ )
+ val markAsFullyRead = DefaultMarkAsFullyRead(
+ FakeMatrixClient(
+ sessionCoroutineScope = backgroundScope,
+ ).apply {
+ givenGetRoomResult(A_ROOM_ID, baseRoom)
+ }
+ )
+ markAsFullyRead.invoke(A_ROOM_ID)
+ runCurrent()
+ markAsReadResult.assertions().isCalledOnce().with(value(ReceiptType.FULLY_READ))
+ baseRoom.assertDestroyed()
+ }
+}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/FakeMarkAsFullyRead.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/FakeMarkAsFullyRead.kt
new file mode 100644
index 0000000000..895676a126
--- /dev/null
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/FakeMarkAsFullyRead.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.messages.impl.timeline
+
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeMarkAsFullyRead(
+ private val invokeResult: (RoomId) -> Unit = { lambdaError() }
+) : MarkAsFullyRead {
+ override fun invoke(roomId: RoomId) {
+ invokeResult(roomId)
+ }
+}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt
index 5e9aa3e214..a95cf4f31f 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt
@@ -29,6 +29,7 @@ import io.element.android.features.poll.test.actions.FakeEndPollAction
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
import io.element.android.features.roomcall.api.aStandByCallState
import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
@@ -40,6 +41,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
+import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID
@@ -58,6 +60,7 @@ import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
+import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -76,7 +79,9 @@ import java.util.Date
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
-@OptIn(ExperimentalCoroutinesApi::class) class TimelinePresenterTest {
+@Suppress("LargeClass")
+@OptIn(ExperimentalCoroutinesApi::class)
+class TimelinePresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@@ -119,9 +124,26 @@ import kotlin.time.Duration.Companion.seconds
}
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
- fun `present - on scroll finished mark a room as read if the first visible index is 0`() = runTest(StandardTestDispatcher()) {
+ fun `present - on scroll finished mark a room as read if the first visible index is 0 - read private`() {
+ `present - on scroll finished mark a room as read if the first visible index is 0`(
+ isSendPublicReadReceiptsEnabled = false,
+ expectedReceiptType = ReceiptType.READ_PRIVATE,
+ )
+ }
+
+ @Test
+ fun `present - on scroll finished mark a room as read if the first visible index is 0 - read`() {
+ `present - on scroll finished mark a room as read if the first visible index is 0`(
+ isSendPublicReadReceiptsEnabled = true,
+ expectedReceiptType = ReceiptType.READ,
+ )
+ }
+
+ private fun `present - on scroll finished mark a room as read if the first visible index is 0`(
+ isSendPublicReadReceiptsEnabled: Boolean,
+ expectedReceiptType: ReceiptType,
+ ) = runTest(StandardTestDispatcher()) {
val timeline = FakeTimeline(
timelineItems = flowOf(
listOf(
@@ -129,11 +151,15 @@ import kotlin.time.Duration.Companion.seconds
)
)
)
+ val markAsReadResult = lambdaRecorder> { Result.success(Unit) }
val room = FakeJoinedRoom(
liveTimeline = timeline,
- baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) })
+ baseRoom = FakeBaseRoom(
+ canUserSendMessageResult = { _, _ -> Result.success(true) },
+ markAsReadResult = markAsReadResult,
+ )
)
- val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
+ val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled)
val presenter = createTimelinePresenter(
timeline = timeline,
room = room,
@@ -145,11 +171,32 @@ import kotlin.time.Duration.Companion.seconds
val initialState = awaitFirstItem()
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
runCurrent()
- assertThat(room.baseRoom.markAsReadCalls).isNotEmpty()
+ assert(markAsReadResult)
+ .isCalledOnce()
+ .with(value(expectedReceiptType))
cancelAndIgnoreRemainingEvents()
}
}
+ @Test
+ fun `present - once presenter is disposed, room is marked as fully read`() = runTest {
+ val invokeResult = lambdaRecorder { }
+ val presenter = createTimelinePresenter(
+ room = FakeJoinedRoom(
+ baseRoom = FakeBaseRoom(
+ canUserSendMessageResult = { _, _ -> Result.success(true) },
+ )
+ ),
+ markAsFullyRead = FakeMarkAsFullyRead(
+ invokeResult = invokeResult,
+ )
+ )
+ presenter.test {
+ awaitFirstItem()
+ }
+ invokeResult.assertions().isCalledOnce().with(value(A_ROOM_ID))
+ }
+
@Test
fun `present - on scroll finished send read receipt if an event is before the index`() = runTest {
val sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType ->
@@ -562,7 +609,7 @@ import kotlin.time.Duration.Companion.seconds
liveTimeline = FakeTimeline(
timelineItems = flowOf(emptyList()),
),
- createTimelineResult = { Result.failure(Throwable("An error")) },
+ createTimelineResult = { Result.failure(RuntimeException("An error")) },
baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) }),
)
)
@@ -674,12 +721,13 @@ import kotlin.time.Duration.Companion.seconds
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
+ markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead(),
): TimelinePresenter {
return TimelinePresenter(
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
room = room,
dispatchers = testCoroutineDispatchers(),
- appScope = this,
+ sessionCoroutineScope = this,
navigator = messagesNavigator,
redactedVoiceMessageManager = redactedVoiceMessageManager,
endPollAction = endPollAction,
@@ -690,6 +738,7 @@ import kotlin.time.Duration.Companion.seconds
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
typingNotificationPresenter = { aTypingNotificationState() },
roomCallStatePresenter = { aStandByCallState() },
+ markAsFullyRead = markAsFullyRead,
)
}
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt
index 110545dcae..c4a2351990 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt
@@ -187,7 +187,7 @@ private fun AndroidComposeTestRule.setTimel
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
forceJumpToBottomVisibility: Boolean = false,
) {
- setSafeContent {
+ setSafeContent(clearAndroidUiDispatcher = true) {
TimelineView(
state = state,
timelineProtectionState = timelineProtectionState,
diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04.kt
index 3de2bb11e5..6023663dac 100644
--- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04.kt
+++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04.kt
@@ -9,6 +9,7 @@ package io.element.android.features.migration.impl.migrations
import android.content.Context
import com.squareup.anvil.annotations.ContributesMultibinding
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import javax.inject.Inject
@@ -26,6 +27,6 @@ class AppMigration04 @Inject constructor(
override val order: Int = 4
override suspend fun migrate() {
- runCatching { context.getDatabasePath(NOTIFICATION_FILE_NAME).delete() }
+ runCatchingExceptions { context.getDatabasePath(NOTIFICATION_FILE_NAME).delete() }
}
}
diff --git a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt
index 6d6952ab14..a69cf4bcbc 100644
--- a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt
+++ b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt
@@ -19,6 +19,7 @@ import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
+import io.element.android.libraries.di.annotations.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose
@@ -38,6 +39,7 @@ import javax.inject.Inject
@SingleIn(AppScope::class)
class DefaultNetworkMonitor @Inject constructor(
@ApplicationContext context: Context,
+ @AppCoroutineScope
appCoroutineScope: CoroutineScope,
) : NetworkMonitor {
private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java)
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 ab711df996..0fbdcdee36 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,6 +7,7 @@
package io.element.android.features.poll.impl.data
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.JoinedRoom
@@ -22,7 +23,7 @@ class PollRepository @Inject constructor(
private val room: JoinedRoom,
private val timelineProvider: TimelineProvider,
) {
- suspend fun getPoll(eventId: EventId): Result = runCatching {
+ suspend fun getPoll(eventId: EventId): Result = runCatchingExceptions {
timelineProvider
.getActiveTimeline()
.timelineItems
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 cef19ef687..e3d7632265 100644
--- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt
+++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt
@@ -23,6 +23,7 @@ import io.element.android.features.poll.impl.history.model.PollHistoryFilter
import io.element.android.features.poll.impl.history.model.PollHistoryItems
import io.element.android.features.poll.impl.history.model.PollHistoryItemsFactory
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import kotlinx.coroutines.CoroutineScope
@@ -31,7 +32,8 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
class PollHistoryPresenter @Inject constructor(
- private val appCoroutineScope: CoroutineScope,
+ @SessionCoroutineScope
+ private val sessionCoroutineScope: CoroutineScope,
private val sendPollResponseAction: SendPollResponseAction,
private val endPollAction: EndPollAction,
private val pollHistoryItemFactory: PollHistoryItemsFactory,
@@ -64,10 +66,10 @@ class PollHistoryPresenter @Inject constructor(
is PollHistoryEvents.LoadMore -> {
coroutineScope.loadMore(timeline)
}
- is PollHistoryEvents.SelectPollAnswer -> appCoroutineScope.launch {
+ is PollHistoryEvents.SelectPollAnswer -> sessionCoroutineScope.launch {
sendPollResponseAction.execute(pollStartId = event.pollStartId, answerId = event.answerId)
}
- is PollHistoryEvents.EndPoll -> appCoroutineScope.launch {
+ is PollHistoryEvents.EndPoll -> sessionCoroutineScope.launch {
endPollAction.execute(pollStartId = event.pollStartId)
}
is PollHistoryEvents.SelectFilter -> {
diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt
index 17d270f5ec..eeb4d0d223 100644
--- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt
+++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt
@@ -163,7 +163,7 @@ class PollHistoryPresenterTest {
),
): PollHistoryPresenter {
return PollHistoryPresenter(
- appCoroutineScope = this,
+ sessionCoroutineScope = this,
sendPollResponseAction = sendPollResponseAction,
endPollAction = endPollAction,
pollHistoryItemFactory = pollHistoryItemFactory,
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt
index 57335c5d97..ba90f93ba5 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt
@@ -21,7 +21,7 @@ class BlockedUsersStateProvider : PreviewParameterProvider {
aBlockedUsersState(blockedUsers = emptyList()),
aBlockedUsersState(unblockUserAction = AsyncAction.ConfirmingNoParams),
aBlockedUsersState(unblockUserAction = AsyncAction.Loading),
- aBlockedUsersState(unblockUserAction = AsyncAction.Failure(Throwable("Failed to unblock user"))),
+ aBlockedUsersState(unblockUserAction = AsyncAction.Failure(RuntimeException("Failed to unblock user"))),
aBlockedUsersState(unblockUserAction = AsyncAction.Success(Unit)),
)
}
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
index cc4ae25eba..9bd3d93dac 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
@@ -29,6 +29,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.featureflag.api.Feature
@@ -201,8 +202,8 @@ class DeveloperSettingsPresenter @Inject constructor(
}
private fun customElementCallUrlValidator(url: String?): Boolean {
- return runCatching {
- if (url.isNullOrEmpty()) return@runCatching
+ return runCatchingExceptions {
+ if (url.isNullOrEmpty()) return@runCatchingExceptions
val parsedUrl = URL(url)
if (parsedUrl.protocol !in listOf("http", "https")) error("Incorrect protocol")
if (parsedUrl.host.isNullOrBlank()) error("Missing host")
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt
index 3c1c81e758..93b7f27b06 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt
@@ -22,6 +22,7 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingStateNoSuccess
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
@@ -209,7 +210,7 @@ class NotificationSettingsPresenter @Inject constructor(
}
private fun CoroutineScope.fixConfigurationMismatch(target: MutableState) = launch {
- runCatching {
+ runCatchingExceptions {
val groupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false).getOrThrow()
val encryptedGroupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false).getOrThrow()
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt
index c0193bd6a2..0b46328b9b 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt
@@ -23,10 +23,10 @@ open class NotificationSettingsStateProvider : PreviewParameterProvider {
- return runCatching {
+ return runCatchingExceptions {
if (avatarUri != null) {
val preprocessed = mediaPreProcessor.process(
uri = avatarUri,
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt
index aa5191d201..b452b9c744 100644
--- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt
+++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt
@@ -14,7 +14,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingPresenter
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingStateEvents
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
-import io.element.android.libraries.matrix.test.A_THROWABLE
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
@@ -81,7 +81,7 @@ class EditDefaultNotificationSettingsPresenterTest {
fun `present - edit default notification setting failed`() = runTest {
val notificationSettingsService = FakeNotificationSettingsService()
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService)
- notificationSettingsService.givenSetDefaultNotificationModeError(A_THROWABLE)
+ notificationSettingsService.givenSetDefaultNotificationModeError(AN_EXCEPTION)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt
index fcefd5c43d..8517d9d285 100644
--- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt
+++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt
@@ -16,7 +16,7 @@ import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermiss
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
-import io.element.android.libraries.matrix.test.A_THROWABLE
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.push.api.PushService
@@ -208,7 +208,7 @@ class NotificationSettingsPresenterTest {
fun `present - clear notification settings change error`() = runTest {
val notificationSettingsService = FakeNotificationSettingsService()
val presenter = createNotificationSettingsPresenter(notificationSettingsService)
- notificationSettingsService.givenSetAtRoomError(A_THROWABLE)
+ notificationSettingsService.givenSetAtRoomError(AN_EXCEPTION)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt
index 47830d2449..01e56e8f59 100644
--- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt
+++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt
@@ -393,7 +393,7 @@ class EditUserProfilePresenterTest {
),
)
fakePickerProvider.givenResult(anotherAvatarUri)
- fakeMediaPreProcessor.givenResult(Result.failure(Throwable("Oh no")))
+ fakeMediaPreProcessor.givenResult(Result.failure(RuntimeException("Oh no")))
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -410,7 +410,7 @@ class EditUserProfilePresenterTest {
fun `present - sets save action to failure if name update fails`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val matrixClient = FakeMatrixClient().apply {
- givenSetDisplayNameResult(Result.failure(Throwable("!")))
+ givenSetDisplayNameResult(Result.failure(RuntimeException("!")))
}
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.UpdateDisplayName("New name"))
}
@@ -419,7 +419,7 @@ class EditUserProfilePresenterTest {
fun `present - sets save action to failure if removing avatar fails`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val matrixClient = FakeMatrixClient().apply {
- givenRemoveAvatarResult(Result.failure(Throwable("!")))
+ givenRemoveAvatarResult(Result.failure(RuntimeException("!")))
}
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
}
@@ -429,7 +429,7 @@ class EditUserProfilePresenterTest {
givenPickerReturnsFile()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val matrixClient = FakeMatrixClient().apply {
- givenUploadAvatarResult(Result.failure(Throwable("!")))
+ givenUploadAvatarResult(Result.failure(RuntimeException("!")))
}
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
}
@@ -439,7 +439,7 @@ class EditUserProfilePresenterTest {
givenPickerReturnsFile()
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val matrixClient = FakeMatrixClient().apply {
- givenSetDisplayNameResult(Result.failure(Throwable("!")))
+ givenSetDisplayNameResult(Result.failure(RuntimeException("!")))
}
val presenter = createEditUserProfilePresenter(matrixUser = user, matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt
index 455cefdb24..f42d252080 100644
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt
@@ -22,6 +22,7 @@ import io.element.android.features.rageshake.impl.crash.CrashDataStore
import io.element.android.features.rageshake.impl.screenshot.ScreenshotHolder
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.di.annotations.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -30,6 +31,7 @@ class BugReportPresenter @Inject constructor(
private val bugReporter: BugReporter,
private val crashDataStore: CrashDataStore,
private val screenshotHolder: ScreenshotHolder,
+ @AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
) : Presenter {
private class BugReporterUploadListener(
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt
index 42b79aed3b..760106b0fb 100644
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt
@@ -41,7 +41,6 @@ import io.element.android.libraries.designsystem.components.preferences.Preferen
import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
@@ -74,8 +73,8 @@ fun BugReportView(
TextField(
value = descriptionFieldState,
modifier = Modifier
- .fillMaxWidth()
- .onTabOrEnterKeyFocusNext(LocalFocusManager.current),
+ .fillMaxWidth()
+ .onTabOrEnterKeyFocusNext(LocalFocusManager.current),
enabled = isFormEnabled,
placeholder = stringResource(id = R.string.screen_bug_report_editor_placeholder),
supportingText = stringResource(id = R.string.screen_bug_report_editor_description),
@@ -139,7 +138,6 @@ fun BugReportView(
modifier = Modifier.fillMaxWidth(fraction = 0.5f),
model = model,
contentDescription = null,
- placeholder = debugPlaceholderBackground(),
)
}
}
@@ -152,8 +150,8 @@ fun BugReportView(
enabled = state.submitEnabled,
showProgress = state.sending.isLoading(),
modifier = Modifier
- .fillMaxWidth()
- .padding(top = 24.dp, bottom = 16.dp)
+ .fillMaxWidth()
+ .padding(top = 24.dp, bottom = 16.dp)
)
}
}
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
index 36ece91408..b9ed1c7778 100755
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
@@ -308,7 +308,7 @@ class DefaultBugReporter @Inject constructor(
*/
private fun getLogFiles(): List {
return tryOrNull(
- onError = { Timber.e(it, "## getLogFiles() failed") }
+ onException = { Timber.e(it, "## getLogFiles() failed") }
) {
val logDirectory = logDirectory()
logDirectory.listFiles()?.toList()
diff --git a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt
index dbcf39766b..dc0faf26f1 100644
--- a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt
+++ b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt
@@ -46,14 +46,18 @@ class RoomCallStatePresenter @Inject constructor(
(currentCall as? CurrentCall.RoomCall)?.roomId == room.roomId
}
}
- val callState = when {
- isAvailable.not() -> RoomCallState.Unavailable
- roomInfo.hasRoomCall -> RoomCallState.OnGoing(
- canJoinCall = canJoinCall,
- isUserInTheCall = isUserInTheCall,
- isUserLocallyInTheCall = isUserLocallyInTheCall,
- )
- else -> RoomCallState.StandBy(canStartCall = canJoinCall)
+ val callState by remember {
+ derivedStateOf {
+ when {
+ isAvailable.not() -> RoomCallState.Unavailable
+ roomInfo.hasRoomCall -> RoomCallState.OnGoing(
+ canJoinCall = canJoinCall,
+ isUserInTheCall = isUserInTheCall,
+ isUserLocallyInTheCall = isUserLocallyInTheCall,
+ )
+ else -> RoomCallState.StandBy(canStartCall = canJoinCall)
+ }
+ }
}
return callState
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt
index 51fda0f7bc..505751c412 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt
@@ -174,7 +174,7 @@ class RoomDetailsPresenter @Inject constructor(
}
val hasMemberVerificationViolations by produceState(false) {
- room.roomMemberIdentityStateChange()
+ room.roomMemberIdentityStateChange(waitForEncryption = true)
.onEach { identities -> value = identities.any { it.identityState == IdentityState.VerificationViolation } }
.launchIn(this)
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt
index fb77530b6d..fb530e601a 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt
@@ -24,6 +24,7 @@ import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.StateEventType
@@ -216,7 +217,7 @@ class RoomDetailsEditPresenter @Inject constructor(
}
private suspend fun updateAvatar(avatarUri: Uri?): Result {
- return runCatching {
+ return runCatchingExceptions {
if (avatarUri != null) {
val preprocessed = mediaPreProcessor.process(
uri = avatarUri,
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt
index 0b979be743..7ae59febc0 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt
@@ -27,7 +27,7 @@ open class RoomDetailsEditStateProvider : PreviewParameterProvider()) {
- room.roomMemberIdentityStateChange()
+ room.roomMemberIdentityStateChange(waitForEncryption = true)
.onEach { identities ->
value = identities.associateBy({ it.identityRoomMember.userId }, { it.identityState }).toPersistentMap()
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt
index 9afe43daf2..55126b33cd 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt
@@ -9,6 +9,7 @@ package io.element.android.features.roomdetails.impl.members.details
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
@@ -33,9 +34,7 @@ import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -86,31 +85,30 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
val userProfileState = userProfilePresenter.present()
- val identityStateChanges by produceState(initialValue = null) {
- room.roomInfoFlow.filter { it.isEncrypted == true }
- .flatMapLatest {
- // Fetch the initial identity state manually
- val identityState = encryptionService.getUserIdentity(roomMemberId).getOrNull()
- value = identityState?.let { IdentityStateChange(roomMemberId, it) }
+ val identityStateChanges = produceState(initialValue = null) {
+ // Fetch the initial identity state manually
+ val identityState = encryptionService.getUserIdentity(roomMemberId).getOrNull()
+ value = identityState?.let { IdentityStateChange(roomMemberId, it) }
- // Subscribe to the identity changes
- room.roomMemberIdentityStateChange()
- .map { it.find { it.identityRoomMember.userId == roomMemberId } }
- .map { roomMemberIdentityStateChange ->
- // If we didn't receive any info, manually fetch it
- roomMemberIdentityStateChange?.identityState ?: encryptionService.getUserIdentity(roomMemberId).getOrNull()
- }
- .filterNotNull()
+ // Subscribe to the identity changes
+ room.roomMemberIdentityStateChange(waitForEncryption = false)
+ .map { it.find { it.identityRoomMember.userId == roomMemberId } }
+ .map { roomMemberIdentityStateChange ->
+ // If we didn't receive any info, manually fetch it
+ roomMemberIdentityStateChange?.identityState ?: encryptionService.getUserIdentity(roomMemberId).getOrNull()
}
+ .filterNotNull()
.collect { value = IdentityStateChange(roomMemberId, it) }
}
- val verificationState = remember(identityStateChanges) {
- when (identityStateChanges?.identityState) {
- IdentityState.VerificationViolation -> UserProfileVerificationState.VERIFICATION_VIOLATION
- IdentityState.Verified -> UserProfileVerificationState.VERIFIED
- IdentityState.Pinned, IdentityState.PinViolation -> UserProfileVerificationState.UNVERIFIED
- else -> UserProfileVerificationState.UNKNOWN
+ val verificationState by remember {
+ derivedStateOf {
+ when (identityStateChanges.value?.identityState) {
+ IdentityState.VerificationViolation -> UserProfileVerificationState.VERIFICATION_VIOLATION
+ IdentityState.Verified -> UserProfileVerificationState.VERIFIED
+ IdentityState.Pinned, IdentityState.PinViolation -> UserProfileVerificationState.UNVERIFIED
+ else -> UserProfileVerificationState.UNKNOWN
+ }
}
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt
index 7df66ad9db..18e95f6ab7 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt
@@ -19,9 +19,9 @@ internal class RoomNotificationSettingsStateProvider : PreviewParameterProvider<
aRoomNotificationSettingsState(),
aRoomNotificationSettingsState(isDefault = false),
aRoomNotificationSettingsState(setNotificationSettingAction = AsyncAction.Loading),
- aRoomNotificationSettingsState(setNotificationSettingAction = AsyncAction.Failure(Throwable("error"))),
+ aRoomNotificationSettingsState(setNotificationSettingAction = AsyncAction.Failure(RuntimeException("error"))),
aRoomNotificationSettingsState(restoreDefaultAction = AsyncAction.Loading),
- aRoomNotificationSettingsState(restoreDefaultAction = AsyncAction.Failure(Throwable("error"))),
+ aRoomNotificationSettingsState(restoreDefaultAction = AsyncAction.Failure(RuntimeException("error"))),
aRoomNotificationSettingsState(displayMentionsOnlyDisclaimer = true)
)
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt
index acd4c28483..74a7d992b7 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt
@@ -64,6 +64,7 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
+import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
@@ -397,7 +398,9 @@ internal fun ChangeRolesViewPreview(@PreviewParameter(ChangeRolesStateProvider::
@PreviewsDayNight
@Composable
internal fun PendingMemberRowWithLongNamePreview() {
- ElementPreview {
+ ElementPreview(
+ drawableFallbackForImages = CommonDrawables.sample_avatar,
+ ) {
MemberRow(
avatarData = AvatarData("userId", "A very long name that should be truncated", "https://example.com/avatar.png", AvatarSize.UserListItem),
name = "A very long name that should be truncated",
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt
index 8de8a52b0f..ee85c61944 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt
@@ -258,7 +258,7 @@ class RoomDetailsPresenterTest {
@Test
fun `present - initial state when canInvite errors`() = runTest {
val room = aJoinedRoom(
- canInviteResult = { Result.failure(Throwable("Whoops")) },
+ canInviteResult = { Result.failure(RuntimeException("Whoops")) },
canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) },
)
@@ -277,7 +277,7 @@ class RoomDetailsPresenterTest {
when (stateEventType) {
StateEventType.ROOM_TOPIC -> Result.success(true)
StateEventType.ROOM_NAME -> Result.success(false)
- else -> Result.failure(Throwable("Whelp"))
+ else -> Result.failure(RuntimeException("Whelp"))
}
},
canBanResult = { Result.success(false) },
@@ -306,7 +306,7 @@ class RoomDetailsPresenterTest {
StateEventType.ROOM_TOPIC,
StateEventType.ROOM_NAME,
StateEventType.ROOM_AVATAR -> Result.success(true)
- else -> Result.failure(Throwable("Whelp"))
+ else -> Result.failure(RuntimeException("Whelp"))
}
},
canKickResult = { Result.success(false) },
@@ -357,7 +357,7 @@ class RoomDetailsPresenterTest {
StateEventType.ROOM_AVATAR,
StateEventType.ROOM_TOPIC,
StateEventType.ROOM_NAME -> Result.success(true)
- else -> Result.failure(Throwable("Whelp"))
+ else -> Result.failure(RuntimeException("Whelp"))
}
},
userDisplayNameResult = { Result.success(A_USER_NAME) },
@@ -403,7 +403,7 @@ class RoomDetailsPresenterTest {
StateEventType.ROOM_TOPIC,
StateEventType.ROOM_NAME,
StateEventType.ROOM_AVATAR -> Result.success(true)
- else -> Result.failure(Throwable("Whelp"))
+ else -> Result.failure(RuntimeException("Whelp"))
}
},
canKickResult = {
@@ -436,7 +436,7 @@ class RoomDetailsPresenterTest {
StateEventType.ROOM_TOPIC,
StateEventType.ROOM_NAME,
StateEventType.ROOM_AVATAR -> Result.success(false)
- else -> Result.failure(Throwable("Whelp"))
+ else -> Result.failure(RuntimeException("Whelp"))
}
},
canBanResult = {
@@ -468,7 +468,7 @@ class RoomDetailsPresenterTest {
StateEventType.ROOM_AVATAR,
StateEventType.ROOM_NAME -> Result.success(true)
StateEventType.ROOM_TOPIC -> Result.success(false)
- else -> Result.failure(Throwable("Whelp"))
+ else -> Result.failure(RuntimeException("Whelp"))
}
},
canKickResult = {
@@ -500,7 +500,7 @@ class RoomDetailsPresenterTest {
StateEventType.ROOM_AVATAR,
StateEventType.ROOM_TOPIC,
StateEventType.ROOM_NAME -> Result.success(true)
- else -> Result.failure(Throwable("Whelp"))
+ else -> Result.failure(RuntimeException("Whelp"))
}
},
canKickResult = {
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenterTest.kt
index 7a2ff4ee06..bb6a9b3027 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenterTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenterTest.kt
@@ -124,7 +124,7 @@ class RoomDetailsEditPresenterTest {
when (stateEventType) {
StateEventType.ROOM_NAME -> Result.success(true)
StateEventType.ROOM_AVATAR -> Result.success(false)
- StateEventType.ROOM_TOPIC -> Result.failure(Throwable("Oops"))
+ StateEventType.ROOM_TOPIC -> Result.failure(RuntimeException("Oops"))
else -> lambdaError()
}
},
@@ -157,7 +157,7 @@ class RoomDetailsEditPresenterTest {
when (stateEventType) {
StateEventType.ROOM_NAME -> Result.success(false)
StateEventType.ROOM_AVATAR -> Result.success(true)
- StateEventType.ROOM_TOPIC -> Result.failure(Throwable("Oops"))
+ StateEventType.ROOM_TOPIC -> Result.failure(RuntimeException("Oops"))
else -> lambdaError()
}
}
@@ -188,7 +188,7 @@ class RoomDetailsEditPresenterTest {
canSendStateResult = { _, stateEventType ->
when (stateEventType) {
StateEventType.ROOM_NAME -> Result.success(false)
- StateEventType.ROOM_AVATAR -> Result.failure(Throwable("Oops"))
+ StateEventType.ROOM_AVATAR -> Result.failure(RuntimeException("Oops"))
StateEventType.ROOM_TOPIC -> Result.success(true)
else -> lambdaError()
}
@@ -559,7 +559,7 @@ class RoomDetailsEditPresenterTest {
canSendStateResult = { _, _ -> Result.success(true) }
)
fakePickerProvider.givenResult(anotherAvatarUri)
- fakeMediaPreProcessor.givenResult(Result.failure(Throwable("Oh no")))
+ fakeMediaPreProcessor.givenResult(Result.failure(RuntimeException("Oh no")))
val deleteCallback = lambdaRecorder {}
val presenter = createRoomDetailsEditPresenter(
room = room,
@@ -580,7 +580,7 @@ class RoomDetailsEditPresenterTest {
topic = "My topic",
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
- setNameResult = { Result.failure(Throwable("!")) },
+ setNameResult = { Result.failure(RuntimeException("!")) },
canSendStateResult = { _, _ -> Result.success(true) }
)
saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomName("New name"), deleteCallbackNumberOfInvocation = 1)
@@ -592,7 +592,7 @@ class RoomDetailsEditPresenterTest {
topic = "My topic",
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
- setTopicResult = { Result.failure(Throwable("!")) },
+ setTopicResult = { Result.failure(RuntimeException("!")) },
canSendStateResult = { _, _ -> Result.success(true) }
)
saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomTopic("New topic"), deleteCallbackNumberOfInvocation = 1)
@@ -604,7 +604,7 @@ class RoomDetailsEditPresenterTest {
topic = "My topic",
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
- removeAvatarResult = { Result.failure(Throwable("!")) },
+ removeAvatarResult = { Result.failure(RuntimeException("!")) },
canSendStateResult = { _, _ -> Result.success(true) }
)
saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove), deleteCallbackNumberOfInvocation = 2)
@@ -617,7 +617,7 @@ class RoomDetailsEditPresenterTest {
topic = "My topic",
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
- updateAvatarResult = { _, _ -> Result.failure(Throwable("!")) },
+ updateAvatarResult = { _, _ -> Result.failure(RuntimeException("!")) },
canSendStateResult = { _, _ -> Result.success(true) }
)
saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto), deleteCallbackNumberOfInvocation = 2)
@@ -630,7 +630,7 @@ class RoomDetailsEditPresenterTest {
topic = "My topic",
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
- setTopicResult = { Result.failure(Throwable("!")) },
+ setTopicResult = { Result.failure(RuntimeException("!")) },
canSendStateResult = { _, _ -> Result.success(true) }
)
val deleteCallback = lambdaRecorder {}
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditViewTest.kt
index 5cc12d8b57..2ac3d4397c 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditViewTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditViewTest.kt
@@ -205,7 +205,7 @@ class RoomDetailsEditViewTest {
rule.setRoomDetailsEditView(
aRoomDetailsEditState(
eventSink = eventsRecorder,
- saveAction = AsyncAction.Failure(Throwable("Whelp")),
+ saveAction = AsyncAction.Failure(RuntimeException("Whelp")),
),
)
rule.clickOn(CommonStrings.action_ok)
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt
index f2436179f7..aff05a55e6 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt
@@ -186,7 +186,7 @@ class RoomMemberListPresenterTest {
val presenter = createPresenter(
joinedRoom = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
- canInviteResult = { Result.failure(Throwable("Eek")) },
+ canInviteResult = { Result.failure(RuntimeException("Eek")) },
updateMembersResult = { Result.success(Unit) }
)
)
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt
index 17d49cfab9..73859b712f 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt
@@ -78,8 +78,8 @@ class RoomMemberDetailsPresenterTest {
avatarUrl = "Alice Avatar url",
)
val room = aJoinedRoom(
- userDisplayNameResult = { Result.failure(Throwable()) },
- userAvatarUrlResult = { Result.failure(Throwable()) },
+ userDisplayNameResult = { Result.failure(RuntimeException()) },
+ userAvatarUrlResult = { Result.failure(RuntimeException()) },
getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) },
).apply {
givenRoomMembersState(RoomMembersState.Ready(persistentListOf(roomMember)))
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenterTest.kt
index 3cfc359792..308ad94379 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenterTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenterTest.kt
@@ -13,8 +13,8 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.roomdetails.impl.aJoinedRoom
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
-import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.tests.testutils.awaitLastSequentialItem
@@ -71,7 +71,7 @@ class RoomNotificationSettingsPresenterTest {
@Test
fun `present - notification settings set custom failed`() = runTest {
val notificationSettingsService = FakeNotificationSettingsService()
- notificationSettingsService.givenSetNotificationModeError(A_THROWABLE)
+ notificationSettingsService.givenSetNotificationModeError(AN_EXCEPTION)
val presenter = createRoomNotificationSettingsPresenter(notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -131,7 +131,7 @@ class RoomNotificationSettingsPresenterTest {
@Test
fun `present - notification settings restore default failed`() = runTest {
val notificationSettingsService = FakeNotificationSettingsService()
- notificationSettingsService.givenRestoreDefaultNotificationModeError(A_THROWABLE)
+ notificationSettingsService.givenRestoreDefaultNotificationModeError(AN_EXCEPTION)
val presenter = createRoomNotificationSettingsPresenter(notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt
index 9d793952b3..8aded9f698 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt
@@ -21,6 +21,7 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledTimes
import io.element.android.tests.testutils.pressBack
+import io.element.android.tests.testutils.setSafeContent
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@@ -159,14 +160,14 @@ class RolesAndPermissionsViewTest {
private fun AndroidComposeTestRule.setRolesAndPermissionsView(
state: RolesAndPermissionsState = aRolesAndPermissionsState(
- eventSink = EventsRecorder(expectEvents = false),
+ eventSink = EventsRecorder(expectEvents = false),
),
goBack: () -> Unit = EnsureNeverCalled(),
openAdminList: () -> Unit = EnsureNeverCalled(),
openModeratorList: () -> Unit = EnsureNeverCalled(),
openPermissionScreens: () -> Unit = EnsureNeverCalled(),
) {
- setContent {
+ setSafeContent {
RolesAndPermissionsView(
state = state,
rolesAndPermissionsNavigator = object : RolesAndPermissionsNavigator {
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt
index da3a3fa4d0..4bb0253e6a 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt
@@ -17,6 +17,7 @@ import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.toMatrixUser
@@ -41,7 +42,7 @@ class ChangeRolesViewTest {
@Test
fun `passing a 'USER' role throws an exception`() {
- val exception = runCatching {
+ val exception = runCatchingExceptions {
rule.setChangeRolesContent(
state = aChangeRolesState(
role = RoomMember.Role.USER,
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt
index e81c8828ae..6d59837988 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt
@@ -12,6 +12,7 @@ import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
import io.element.android.libraries.androidutils.system.DateTimeObserver
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
@@ -36,7 +37,8 @@ class RoomListDataSource @Inject constructor(
private val roomListRoomSummaryFactory: RoomListRoomSummaryFactory,
private val coroutineDispatchers: CoroutineDispatchers,
private val notificationSettingsService: NotificationSettingsService,
- private val appScope: CoroutineScope,
+ @SessionCoroutineScope
+ private val sessionCoroutineScope: CoroutineScope,
private val dateTimeObserver: DateTimeObserver,
) {
init {
@@ -77,7 +79,7 @@ class RoomListDataSource @Inject constructor(
.onEach {
roomListService.allRooms.rebuildSummaries()
}
- .launchIn(appScope)
+ .launchIn(sessionCoroutineScope)
}
private fun observeDateTimeChanges() {
@@ -88,7 +90,7 @@ class RoomListDataSource @Inject constructor(
is DateTimeObserver.Event.DateChanged -> rebuildAllRoomSummaries()
}
}
- .launchIn(appScope)
+ .launchIn(sessionCoroutineScope)
}
private suspend fun replaceWith(roomSummaries: List) = withContext(coroutineDispatchers.computation) {
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt
index 80b5a87c10..6becb39e7a 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt
@@ -8,6 +8,7 @@
package io.element.android.features.roomlist.impl
import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.matrix.api.core.RoomId
@@ -16,6 +17,7 @@ import io.element.android.tests.testutils.EnsureCalledOnceWithParam
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.setSafeContent
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -28,15 +30,10 @@ class RoomListContextMenuTest {
fun `clicking on Mark as read generates expected Events`() {
val eventsRecorder = EventsRecorder()
val contextMenu = aContextMenuShown(hasNewContent = true)
- rule.setContent {
- RoomListContextMenu(
- contextMenu = contextMenu,
- canReportRoom = false,
- eventSink = eventsRecorder,
- onRoomSettingsClick = EnsureNeverCalledWithParam(),
- onReportRoomClick = EnsureNeverCalledWithParam(),
- )
- }
+ rule.setRoomListContextMenu(
+ contextMenu = contextMenu,
+ eventSink = eventsRecorder,
+ )
rule.clickOn(R.string.screen_roomlist_mark_as_read)
eventsRecorder.assertList(
listOf(
@@ -50,15 +47,10 @@ class RoomListContextMenuTest {
fun `clicking on Mark as unread generates expected Events`() {
val eventsRecorder = EventsRecorder()
val contextMenu = aContextMenuShown(hasNewContent = false)
- rule.setContent {
- RoomListContextMenu(
- contextMenu = contextMenu,
- canReportRoom = false,
- eventSink = eventsRecorder,
- onRoomSettingsClick = EnsureNeverCalledWithParam(),
- onReportRoomClick = EnsureNeverCalledWithParam(),
- )
- }
+ rule.setRoomListContextMenu(
+ contextMenu = contextMenu,
+ eventSink = eventsRecorder,
+ )
rule.clickOn(R.string.screen_roomlist_mark_as_unread)
eventsRecorder.assertList(
listOf(
@@ -72,15 +64,10 @@ class RoomListContextMenuTest {
fun `clicking on Leave room generates expected Events`() {
val eventsRecorder = EventsRecorder()
val contextMenu = aContextMenuShown(isDm = false)
- rule.setContent {
- RoomListContextMenu(
- contextMenu = contextMenu,
- canReportRoom = false,
- eventSink = eventsRecorder,
- onRoomSettingsClick = EnsureNeverCalledWithParam(),
- onReportRoomClick = EnsureNeverCalledWithParam(),
- )
- }
+ rule.setRoomListContextMenu(
+ contextMenu = contextMenu,
+ eventSink = eventsRecorder,
+ )
rule.clickOn(CommonStrings.action_leave_room)
eventsRecorder.assertList(
listOf(
@@ -95,15 +82,13 @@ class RoomListContextMenuTest {
val eventsRecorder = EventsRecorder()
val contextMenu = aContextMenuShown()
val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit)
- rule.setContent {
- RoomListContextMenu(
- contextMenu = contextMenu,
- canReportRoom = true,
- eventSink = eventsRecorder,
- onRoomSettingsClick = EnsureNeverCalledWithParam(),
- onReportRoomClick = callback,
- )
- }
+ rule.setRoomListContextMenu(
+ contextMenu = contextMenu,
+ canReportRoom = true,
+ eventSink = eventsRecorder,
+ onRoomSettingsClick = EnsureNeverCalledWithParam(),
+ onReportRoomClick = callback,
+ )
rule.clickOn(CommonStrings.action_report_room)
eventsRecorder.assertSingle(RoomListEvents.HideContextMenu)
callback.assertSuccess()
@@ -114,15 +99,11 @@ class RoomListContextMenuTest {
val eventsRecorder = EventsRecorder()
val contextMenu = aContextMenuShown()
val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit)
- rule.setContent {
- RoomListContextMenu(
- contextMenu = contextMenu,
- canReportRoom = false,
- eventSink = eventsRecorder,
- onRoomSettingsClick = callback,
- onReportRoomClick = EnsureNeverCalledWithParam(),
- )
- }
+ rule.setRoomListContextMenu(
+ contextMenu = contextMenu,
+ eventSink = eventsRecorder,
+ onRoomSettingsClick = callback,
+ )
rule.clickOn(CommonStrings.common_settings)
eventsRecorder.assertSingle(RoomListEvents.HideContextMenu)
callback.assertSuccess()
@@ -133,15 +114,11 @@ class RoomListContextMenuTest {
val eventsRecorder = EventsRecorder()
val contextMenu = aContextMenuShown(isDm = false, isFavorite = false)
val callback = EnsureNeverCalledWithParam()
- rule.setContent {
- RoomListContextMenu(
- contextMenu = contextMenu,
- canReportRoom = false,
- eventSink = eventsRecorder,
- onRoomSettingsClick = callback,
- onReportRoomClick = EnsureNeverCalledWithParam(),
- )
- }
+ rule.setRoomListContextMenu(
+ contextMenu = contextMenu,
+ eventSink = eventsRecorder,
+ onRoomSettingsClick = callback,
+ )
rule.clickOn(CommonStrings.common_favourite)
eventsRecorder.assertList(
listOf(
@@ -149,4 +126,22 @@ class RoomListContextMenuTest {
)
)
}
+
+ private fun AndroidComposeTestRule<*, *>.setRoomListContextMenu(
+ contextMenu: RoomListState.ContextMenu.Shown,
+ canReportRoom: Boolean = false,
+ eventSink: (RoomListEvents) -> Unit,
+ onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
+ onReportRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
+ ) {
+ setSafeContent {
+ RoomListContextMenu(
+ contextMenu = contextMenu,
+ canReportRoom = canReportRoom,
+ onRoomSettingsClick = onRoomSettingsClick,
+ onReportRoomClick = onReportRoomClick,
+ eventSink = eventSink,
+ )
+ }
+ }
}
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenuTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenuTest.kt
index 0fdb5d7639..8c75027ec5 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenuTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenuTest.kt
@@ -16,6 +16,7 @@ import io.element.android.tests.testutils.EnsureCalledOnceWithParam
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.setSafeContent
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -28,7 +29,7 @@ class RoomListDeclineInviteMenuTest {
fun `clicking on decline emits the expected Events`() {
val eventsRecorder = EventsRecorder()
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
- rule.setContent {
+ rule.setSafeContent {
RoomListDeclineInviteMenu(
menu = menu,
canReportRoom = false,
@@ -49,7 +50,7 @@ class RoomListDeclineInviteMenuTest {
fun `clicking on decline and block when canReportRoom=true, it emits the expected Events and callback`() {
val eventsRecorder = EventsRecorder()
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
- rule.setContent {
+ rule.setSafeContent {
RoomListDeclineInviteMenu(
menu = menu,
canReportRoom = true,
@@ -66,7 +67,7 @@ class RoomListDeclineInviteMenuTest {
fun `clicking on decline and block when canReportRoom=false, it emits the expected Events`() {
val eventsRecorder = EventsRecorder()
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
- rule.setContent {
+ rule.setSafeContent {
RoomListDeclineInviteMenu(
menu = menu,
canReportRoom = false,
@@ -86,7 +87,7 @@ class RoomListDeclineInviteMenuTest {
fun `clicking on cancel emits the expected Event`() {
val eventsRecorder = EventsRecorder()
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
- rule.setContent {
+ rule.setSafeContent {
RoomListDeclineInviteMenu(
menu = menu,
canReportRoom = false,
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
index 5726eb1eb0..446a864f0b 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
@@ -504,9 +504,18 @@ class RoomListPresenterTest {
@Test
fun `present - check that the room is marked as read with correct RR and as unread`() = runTest {
- val room = FakeBaseRoom()
- val room2 = FakeBaseRoom(roomId = A_ROOM_ID_2)
- val room3 = FakeBaseRoom(roomId = A_ROOM_ID_3)
+ val markAsReadResult = lambdaRecorder> { Result.success(Unit) }
+ val markAsReadResult3 = lambdaRecorder> { Result.success(Unit) }
+ val room = FakeBaseRoom(
+ markAsReadResult = markAsReadResult,
+ )
+ val room2 = FakeBaseRoom(
+ roomId = A_ROOM_ID_2,
+ )
+ val room3 = FakeBaseRoom(
+ roomId = A_ROOM_ID_3,
+ markAsReadResult = markAsReadResult3,
+ )
val allRooms = setOf(room, room2, room3)
val sessionPreferencesStore = InMemorySessionPreferencesStore()
val matrixClient = FakeMatrixClient().apply {
@@ -530,21 +539,19 @@ class RoomListPresenterTest {
}.test {
val initialState = awaitItem()
allRooms.forEach {
- assertThat(it.markAsReadCalls).isEmpty()
assertThat(it.setUnreadFlagCalls).isEmpty()
}
initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID))
- assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ))
+ markAsReadResult.assertions().isCalledOnce().with(value(ReceiptType.READ))
assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false))
clearMessagesForRoomLambda.assertions().isCalledOnce()
.with(value(A_SESSION_ID), value(A_ROOM_ID))
initialState.eventSink.invoke(RoomListEvents.MarkAsUnread(A_ROOM_ID_2))
- assertThat(room2.markAsReadCalls).isEmpty()
assertThat(room2.setUnreadFlagCalls).isEqualTo(listOf(true))
// Test again with private read receipts
sessionPreferencesStore.setSendPublicReadReceipts(false)
initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID_3))
- assertThat(room3.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ_PRIVATE))
+ markAsReadResult3.assertions().isCalledOnce().with(value(ReceiptType.READ_PRIVATE))
assertThat(room3.setUnreadFlagCalls).isEqualTo(listOf(false))
clearMessagesForRoomLambda.assertions().isCalledExactly(2)
.withSequence(
@@ -694,7 +701,7 @@ class RoomListPresenterTest {
),
coroutineDispatchers = testCoroutineDispatchers(),
notificationSettingsService = client.notificationSettingsService(),
- appScope = backgroundScope,
+ sessionCoroutineScope = backgroundScope,
dateTimeObserver = FakeDateTimeObserver(),
),
featureFlagService = featureFlagService,
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt
index 0242591f55..299ef07578 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt
@@ -29,6 +29,7 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam
+import io.element.android.tests.testutils.setSafeContent
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Rule
import org.junit.Test
@@ -52,7 +53,7 @@ class RoomListViewTest {
eventsRecorder.assertList(
listOf(
RoomListEvents.UpdateVisibleRange(IntRange.EMPTY),
- RoomListEvents.UpdateVisibleRange(0 until 2),
+ RoomListEvents.UpdateVisibleRange(0..2),
)
)
}
@@ -273,7 +274,7 @@ private fun AndroidComposeTestRule.setRoomL
onReportRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onDeclineInviteAndBlockUser: (RoomListRoomSummary) -> Unit = EnsureNeverCalledWithParam(),
) {
- setContent {
+ setSafeContent {
RoomListView(
state = state,
onRoomClick = onRoomClick,
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt
index dcf6cdc032..bcb6552d30 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt
@@ -100,7 +100,7 @@ class RoomListDataSourceTest {
roomListRoomSummaryFactory = roomListRoomSummaryFactory,
coroutineDispatchers = testCoroutineDispatchers(),
notificationSettingsService = notificationSettingsService,
- appScope = backgroundScope,
+ sessionCoroutineScope = backgroundScope,
dateTimeObserver = dateTimeObserver,
)
}
diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt
index 5a99607729..c5a394338a 100644
--- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt
+++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt
@@ -256,8 +256,9 @@ private fun RoomMemberActionsBottomSheet(
when (val action = actionState.action) {
is ModerationAction.DisplayProfile -> {
ListItem(
+ style = ListItemStyle.Primary,
headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_member_user_info)) },
- leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())),
+ leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())),
onClick = {
coroutineScope.launch {
onSelectAction(action, user)
diff --git a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt
index d17b7579b6..fc1b815562 100644
--- a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt
+++ b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt
@@ -22,6 +22,7 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnceWithTwoParams
import io.element.android.tests.testutils.pressTag
+import io.element.android.tests.testutils.setSafeContent
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
@@ -217,7 +218,7 @@ private fun AndroidComposeTestRule.setRoomM
state: InternalRoomMemberModerationState,
onSelectAction: (ModerationAction, MatrixUser) -> Unit = EnsureNeverCalledWithTwoParams(),
) {
- setContent {
+ setSafeContent {
RoomMemberModerationView(
state = state,
onSelectAction = onSelectAction,
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt
index c8e9072c6b..6ce66d149d 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt
@@ -26,6 +26,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
+import io.element.android.compound.theme.ElementTheme
import io.element.android.features.securebackup.impl.reset.password.ResetIdentityPasswordNode
import io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootNode
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
@@ -35,9 +36,9 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
-import io.element.android.libraries.oidc.api.OidcEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
@@ -50,8 +51,8 @@ class ResetIdentityFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val resetIdentityFlowManager: ResetIdentityFlowManager,
- private val coroutineScope: CoroutineScope,
- private val oidcEntryPoint: OidcEntryPoint,
+ @SessionCoroutineScope
+ private val sessionCoroutineScope: CoroutineScope,
) : BaseFlowNode(
backstack = BackStack(initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap),
buildContext = buildContext,
@@ -67,12 +68,10 @@ class ResetIdentityFlowNode @AssistedInject constructor(
@Parcelize
data object ResetPassword : NavTarget
-
- @Parcelize
- data class ResetOidc(val url: String) : NavTarget
}
private lateinit var activity: Activity
+ private var darkTheme: Boolean = false
private var resetJob: Job? = null
override fun onBuilt() {
@@ -80,9 +79,9 @@ class ResetIdentityFlowNode @AssistedInject constructor(
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
- // If the custom tab was opened, we need to cancel the reset job
+ // If the custom tab / Web browser was opened, we need to cancel the reset job
// when we come back to the node if the reset wasn't successful
- coroutineScope.launch {
+ sessionCoroutineScope.launch {
cancelResetJob()
resetIdentityFlowManager.whenResetIsDone {
@@ -93,7 +92,7 @@ class ResetIdentityFlowNode @AssistedInject constructor(
override fun onDestroy(owner: LifecycleOwner) {
// Make sure we cancel the reset job when the node is destroyed, just in case
- coroutineScope.launch { cancelResetJob() }
+ sessionCoroutineScope.launch { cancelResetJob() }
}
})
}
@@ -103,7 +102,7 @@ class ResetIdentityFlowNode @AssistedInject constructor(
is NavTarget.Root -> {
val callback = object : ResetIdentityRootNode.Callback {
override fun onContinue() {
- coroutineScope.startReset()
+ sessionCoroutineScope.startReset()
}
}
createNode(buildContext, listOf(callback))
@@ -115,9 +114,6 @@ class ResetIdentityFlowNode @AssistedInject constructor(
listOf(ResetIdentityPasswordNode.Inputs(handle))
)
}
- is NavTarget.ResetOidc -> {
- oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.url)
- }
}
}
@@ -135,11 +131,7 @@ class ResetIdentityFlowNode @AssistedInject constructor(
Timber.d("No reset handle return, the reset is done.")
}
is IdentityOidcResetHandle -> {
- if (oidcEntryPoint.canUseCustomTab()) {
- activity.openUrlInChromeCustomTab(null, false, handle.url)
- } else {
- backstack.push(NavTarget.ResetOidc(handle.url))
- }
+ activity.openUrlInChromeCustomTab(null, darkTheme, handle.url)
resetJob = launch { handle.resetOidc() }
}
is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword)
@@ -162,12 +154,12 @@ class ResetIdentityFlowNode @AssistedInject constructor(
if (!this::activity.isInitialized) {
activity = requireNotNull(LocalActivity.current)
}
-
+ darkTheme = !ElementTheme.isLightTheme
val startResetState by resetIdentityFlowManager.currentHandleFlow.collectAsState()
if (startResetState.isLoading()) {
ProgressDialog(
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true),
- onDismissRequest = { coroutineScope.launch { cancelResetJob() } }
+ onDismissRequest = { sessionCoroutineScope.launch { cancelResetJob() } }
)
}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt
index ecd9cd522f..9c24d463ba 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt
@@ -24,10 +24,12 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.AutofillType
+import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentType
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -39,7 +41,6 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.securebackup.impl.R
import io.element.android.features.securebackup.impl.tools.RecoveryKeyVisualTransformation
-import io.element.android.libraries.designsystem.modifiers.autofill
import io.element.android.libraries.designsystem.modifiers.clickableIfNotNull
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -186,10 +187,9 @@ private fun RecoveryKeyFormContent(
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.recoveryKey)
- .autofill(
- autofillTypes = listOf(AutofillType.Password),
- onFill = { onChange(it) },
- ),
+ .semantics {
+ contentType = ContentType.Password
+ },
minLines = 2,
value = state.formattedRecoveryKey.orEmpty(),
onValueChange = onChange,
diff --git a/features/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 1c75daa828..6dac12a6e2 100644
--- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt
+++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt
@@ -18,6 +18,7 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
@@ -27,10 +28,12 @@ import io.element.android.libraries.preferences.api.store.SessionPreferencesStor
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import kotlin.coroutines.cancellation.CancellationException
class SharePresenter @AssistedInject constructor(
@Assisted private val intent: Intent,
- private val appCoroutineScope: CoroutineScope,
+ @SessionCoroutineScope
+ private val sessionCoroutineScope: CoroutineScope,
private val shareIntentHandler: ShareIntentHandler,
private val matrixClient: MatrixClient,
private val mediaPreProcessor: MediaPreProcessor,
@@ -45,7 +48,7 @@ class SharePresenter @AssistedInject constructor(
private val shareActionState: MutableState>> = mutableStateOf(AsyncAction.Uninitialized)
fun onRoomSelected(roomIds: List) {
- appCoroutineScope.share(intent, roomIds)
+ sessionCoroutineScope.share(intent, roomIds)
}
@Composable
@@ -89,12 +92,21 @@ class SharePresenter @AssistedInject constructor(
)
filesToShare
.map { fileToShare ->
- mediaSender.sendMedia(
+ val result = mediaSender.sendMedia(
uri = fileToShare.uri,
mimeType = fileToShare.mimeType,
- ).isSuccess
+ )
+ // If the coroutine was cancelled, destroy the room and rethrow the exception
+ val cancellationException = result.exceptionOrNull() as? CancellationException
+ if (cancellationException != null) {
+ if (activeRoomsHolder.getActiveRoomMatching(matrixClient.sessionId, roomId) == null) {
+ room.destroy()
+ }
+ throw cancellationException
+ }
+ result.isSuccess
}
- .all { it }
+ .all { isSuccess -> isSuccess }
.also {
if (activeRoomsHolder.getActiveRoomMatching(matrixClient.sessionId, roomId) == null) {
room.destroy()
diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareStateProvider.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareStateProvider.kt
index 62664cdd0d..5afc622fb9 100644
--- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareStateProvider.kt
+++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareStateProvider.kt
@@ -24,7 +24,7 @@ open class ShareStateProvider : PreviewParameterProvider {
)
),
aShareState(
- shareAction = AsyncAction.Failure(Throwable("error")),
+ shareAction = AsyncAction.Failure(RuntimeException("error")),
),
)
}
diff --git a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt
index 07424aa384..979d6aa61c 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
@@ -169,7 +169,7 @@ class SharePresenterTest {
): SharePresenter {
return SharePresenter(
intent = intent,
- appCoroutineScope = this,
+ sessionCoroutineScope = this,
shareIntentHandler = shareIntentHandler,
matrixClient = matrixClient,
mediaPreProcessor = mediaPreProcessor,
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 237c57fecf..c743ebfd40 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
@@ -55,7 +55,7 @@ class UserProfilePresenter @AssistedInject constructor(
@Composable
private fun getDmRoomId(): State {
return produceState(initialValue = null) {
- value = client.findDM(userId)
+ value = client.findDM(userId).getOrNull()
}
}
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 d0369da660..c88872374d 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
@@ -30,7 +30,6 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
-import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
@@ -142,7 +141,7 @@ class UserProfilePresenterTest {
if (canFindRoom) {
givenGetRoomResult(A_ROOM_ID, room)
}
- givenFindDmResult(dmRoom)
+ givenFindDmResult(Result.success(dmRoom))
}
val presenter = createUserProfilePresenter(
userId = A_USER_ID_2,
@@ -215,7 +214,7 @@ class UserProfilePresenterTest {
@Test
fun `present - BlockUser with error`() = runTest {
val matrixClient = createFakeMatrixClient(
- ignoreUserResult = { Result.failure(A_THROWABLE) }
+ ignoreUserResult = { Result.failure(AN_EXCEPTION) }
)
val presenter = createUserProfilePresenter(client = matrixClient)
presenter.test {
@@ -223,7 +222,7 @@ class UserProfilePresenterTest {
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
val errorState = awaitItem()
- assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE)
+ assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(AN_EXCEPTION)
// Clear error
initialState.eventSink(UserProfileEvents.ClearBlockUserError)
assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(false))
@@ -233,7 +232,7 @@ class UserProfilePresenterTest {
@Test
fun `present - UnblockUser with error`() = runTest {
val matrixClient = createFakeMatrixClient(
- unIgnoreUserResult = { Result.failure(A_THROWABLE) }
+ unIgnoreUserResult = { Result.failure(AN_EXCEPTION) }
)
val presenter = createUserProfilePresenter(client = matrixClient)
presenter.test {
@@ -241,7 +240,7 @@ class UserProfilePresenterTest {
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
val errorState = awaitItem()
- assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE)
+ assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(AN_EXCEPTION)
// Clear error
initialState.eventSink(UserProfileEvents.ClearBlockUserError)
assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(true))
@@ -265,7 +264,7 @@ class UserProfilePresenterTest {
@Test
fun `present - start DM action failure scenario`() = runTest {
- val startDMFailureResult = AsyncAction.Failure(A_THROWABLE)
+ val startDMFailureResult = AsyncAction.Failure(AN_EXCEPTION)
val executeResult = lambdaRecorder>, Unit> { _, _, actionState ->
actionState.value = startDMFailureResult
}
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt
index 3b10499b39..4210e3559c 100644
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt
@@ -28,6 +28,7 @@ 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.Text
+import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.matrix.api.core.UserId
@Composable
@@ -42,7 +43,8 @@ fun VerificationUserProfileContent(
}
Row(
- modifier = modifier.fillMaxWidth()
+ modifier = modifier
+ .fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(ElementTheme.colors.bgSubtleSecondary)
.padding(12.dp),
@@ -64,7 +66,9 @@ fun VerificationUserProfileContent(
@PreviewsDayNight
@Composable
-internal fun VerificationUserProfileContentPreview() = ElementPreview {
+internal fun VerificationUserProfileContentPreview() = ElementPreview(
+ drawableFallbackForImages = CommonDrawables.sample_avatar
+) {
VerificationUserProfileContent(
userId = UserId("@alice:example.com"),
displayName = "Alice",
diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt
index eaa51d33ec..06940f37a5 100644
--- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt
+++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt
@@ -206,6 +206,7 @@ class OutgoingVerificationPresenterTest {
)
)
service.emitVerificationFlowState(VerificationFlowState.DidFinish)
+ service.emitVerifiedStatus(SessionVerifiedStatus.Verified)
assertThat(awaitItem().step).isEqualTo(Step.Completed)
}
}
diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContentReader.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContentReader.kt
index a7a9f12456..e94bde4a53 100644
--- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContentReader.kt
+++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContentReader.kt
@@ -9,6 +9,7 @@ package io.element.android.features.viewfolder.impl.file
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.AppScope
import kotlinx.coroutines.withContext
import java.io.File
@@ -23,7 +24,7 @@ class DefaultFileContentReader @Inject constructor(
private val dispatchers: CoroutineDispatchers,
) : FileContentReader {
override suspend fun getLines(path: String): Result> = withContext(dispatchers.io) {
- runCatching {
+ runCatchingExceptions {
File(path).readLines()
}
}
diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileSave.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileSave.kt
index 4337ffd13f..78648c6738 100644
--- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileSave.kt
+++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileSave.kt
@@ -16,6 +16,7 @@ import androidx.annotation.RequiresApi
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.system.toast
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
@@ -40,7 +41,7 @@ class DefaultFileSave @Inject constructor(
path: String,
) {
withContext(dispatchers.io) {
- runCatching {
+ runCatchingExceptions {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
saveOnDiskUsingMediaStore(path)
} else {
diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileShare.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileShare.kt
index 393139d182..471f2df5d4 100644
--- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileShare.kt
+++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileShare.kt
@@ -13,6 +13,7 @@ import android.net.Uri
import androidx.core.content.FileProvider
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.AppScope
@@ -37,7 +38,7 @@ class DefaultFileShare @Inject constructor(
override suspend fun share(
path: String,
) {
- runCatching {
+ runCatchingExceptions {
val file = File(path)
val shareableUri = file.toShareableUri()
val shareMediaIntent = Intent(Intent.ACTION_SEND)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 33cc6b8095..cd95972e97 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -14,13 +14,13 @@ core = "1.16.0"
datastore = "1.1.7"
constraintlayout = "2.2.1"
constraintlayout_compose = "1.1.1"
-lifecycle = "2.9.0"
+lifecycle = "2.9.1"
activity = "1.10.1"
media3 = "1.7.1"
camera = "1.4.2"
# Compose
-compose_bom = "2025.04.00"
+compose_bom = "2025.05.01"
composecompiler = "1.5.15"
# Coroutines
@@ -37,12 +37,13 @@ datetime = "0.6.2"
serialization_json = "1.8.1"
#other
-coil = "3.1.0"
+detekt = "1.23.8"
+coil = "3.2.0"
showkase = "1.0.3"
-appyx = "1.7.0"
+appyx = "1.7.1"
sqldelight = "2.1.0"
wysiwyg = "2.38.3"
-telephoto = "0.15.1"
+telephoto = "0.16.0"
# Dependency analysis
dependencyAnalysis = "2.18.0"
@@ -72,7 +73,7 @@ kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", ve
ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
gms_google_services = "com.google.gms:google-services:4.4.2"
# https://firebase.google.com/docs/android/setup#available-libraries
-google_firebase_bom = "com.google.firebase:firebase-bom:33.14.0"
+google_firebase_bom = "com.google.firebase:firebase-bom:33.15.0"
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
@@ -104,7 +105,7 @@ androidx_activity_activity = { module = "androidx.activity:activity", version.re
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
androidx_startup = "androidx.startup:startup-runtime:1.2.0"
androidx_preference = "androidx.preference:preference:1.2.1"
-androidx_webkit = "androidx.webkit:webkit:1.13.0"
+androidx_webkit = "androidx.webkit:webkit:1.14.0"
androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" }
androidx_compose_material3 = { module = "androidx.compose.material3:material3" }
@@ -152,6 +153,8 @@ test_parameter_injector = "com.google.testparameterinjector:test-parameter-injec
test_robolectric = "org.robolectric:robolectric:4.14.1"
test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" }
test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.6.1"
+test_detekt_api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" }
+test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" }
# Others
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
@@ -169,7 +172,7 @@ jsoup = "org.jsoup:jsoup:1.20.1"
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.6.3"
+matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.6.6"
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" }
@@ -183,15 +186,15 @@ vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.2.2"
-maplibre = "org.maplibre.gl:android-sdk:11.8.8"
+maplibre = "org.maplibre.gl:android-sdk:11.10.1"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
-opusencoder = "io.element.android:opusencoder:1.1.0"
+opusencoder = "io.element.android:opusencoder:1.2.0"
zxing_cpp = "io.github.zxing-cpp:android:2.3.0"
# Analytics
-posthog = "com.posthog:posthog-android:3.16.0"
-sentry = "io.sentry:sentry-android:8.12.0"
+posthog = "com.posthog:posthog-android:3.17.0"
+sentry = "io.sentry:sentry-android:8.13.2"
# main branch can be tested replacing the version with main-SNAPSHOT
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
@@ -207,7 +210,7 @@ anvil_compiler_api = { module = "dev.zacsweers.anvil:compiler-api", version.ref
anvil_compiler_utils = { module = "dev.zacsweers.anvil:compiler-utils", version.ref = "anvil" }
# Element Call
-element_call_embedded = "io.element.android:element-call-embedded:0.12.0"
+element_call_embedded = "io.element.android:element-call-embedded:0.12.2"
# Auto services
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }
@@ -231,7 +234,7 @@ kotlin_serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
anvil = { id = "dev.zacsweers.anvil", version.ref = "anvil" }
-detekt = "io.gitlab.arturbosch.detekt:1.23.8"
+detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
ktlint = "org.jlleitschuh.gradle.ktlint:12.3.0"
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
dependencycheck = "org.owasp.dependencycheck:12.1.1"
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 9128c7d428..3735f265b9 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionSha256Sum=845952a9d6afa783db70bb3b0effaae45ae5542ca2bb7929619e8af49cb634cf
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
+distributionSha256Sum=7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt
deleted file mode 100644
index 5e77d0d00a..0000000000
--- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2025 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.libraries.androidutils.compat
-
-import android.media.AudioDeviceInfo
-import android.media.AudioManager
-import android.os.Build
-import io.element.android.libraries.core.data.tryOrNull
-import timber.log.Timber
-
-fun AudioManager.enableExternalAudioDevice() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- // The list of device types that are considered as communication devices, sorted by likelihood of it being used for communication.
- val wantedDeviceTypes = listOf(
- // Paired bluetooth device with microphone
- AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
- // USB devices which can play or record audio
- AudioDeviceInfo.TYPE_USB_HEADSET,
- AudioDeviceInfo.TYPE_USB_DEVICE,
- AudioDeviceInfo.TYPE_USB_ACCESSORY,
- // Wired audio devices
- AudioDeviceInfo.TYPE_WIRED_HEADSET,
- AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
- // The built-in speaker of the device
- AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
- // The built-in earpiece of the device
- AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
- )
- val devices = availableCommunicationDevices
- val selectedDevice = devices.minByOrNull {
- wantedDeviceTypes.indexOf(it.type).let { index ->
- // If the device type is not in the wantedDeviceTypes list, we give it a low priority
- if (index == -1) Int.MAX_VALUE else index
- }
- }
- selectedDevice?.let { device ->
- Timber.d("Audio device selected, type: ${device.type}")
- tryOrNull(
- onError = { failure ->
- Timber.e(failure, "Audio: exception when setting communication device")
- }
- ) {
- setCommunicationDevice(device).also {
- if (!it) {
- Timber.w("Audio: unable to set the communication device")
- }
- }
- }
- }
- } else {
- // If we don't have access to the new APIs, use the deprecated ones
- @Suppress("DEPRECATION")
- isSpeakerphoneOn = true
- }
-}
-
-fun AudioManager.disableExternalAudioDevice() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- clearCommunicationDevice()
- } else {
- // If we don't have access to the new APIs, use the deprecated ones
- @Suppress("DEPRECATION")
- isSpeakerphoneOn = false
- }
-}
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt
index 8697ae9da7..eaa0b578bc 100644
--- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt
@@ -12,6 +12,7 @@ import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import androidx.core.net.toFile
+import io.element.android.libraries.core.extensions.runCatchingExceptions
fun Context.getMimeType(uri: Uri): String? = when (uri.scheme) {
ContentResolver.SCHEME_CONTENT -> contentResolver.getType(uri)
@@ -32,14 +33,14 @@ fun Context.getFileSize(uri: Uri): Long {
} ?: 0
}
-private fun Context.getContentFileSize(uri: Uri): Long? = runCatching {
+private fun Context.getContentFileSize(uri: Uri): Long? = runCatchingExceptions {
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
cursor.moveToFirst()
return@use cursor.getColumnIndexOrThrow(OpenableColumns.SIZE).let(cursor::getLong)
}
}.getOrNull()
-private fun Context.getContentFileName(uri: Uri): String? = runCatching {
+private fun Context.getContentFileName(uri: Uri): String? = runCatchingExceptions {
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
cursor.moveToFirst()
return@use cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME).let(cursor::getString)
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt
index ef55ed2e08..414b123129 100644
--- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt
@@ -17,7 +17,7 @@ import java.util.UUID
fun File.safeDelete() {
if (exists().not()) return
tryOrNull(
- onError = {
+ onException = {
Timber.e(it, "Error, unable to delete file $path")
},
operation = {
@@ -30,7 +30,7 @@ fun File.safeDelete() {
fun File.safeRenameTo(dest: File) {
tryOrNull(
- onError = {
+ onException = {
Timber.e(it, "Error, unable to rename file $path to ${dest.path}")
},
operation = {
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt
index 5c24925be2..c0cb5a766a 100644
--- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt
@@ -13,6 +13,7 @@ import android.text.util.Linkify
import androidx.core.text.getSpans
import androidx.core.text.toSpannable
import androidx.core.text.util.LinkifyCompat
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import timber.log.Timber
import kotlin.collections.component1
import kotlin.collections.component2
@@ -48,7 +49,7 @@ object LinkifyHelper {
// Try to avoid including trailing punctuation in the link.
// Since this might fail in some edge cases, we catch the exception and just use the original end index.
- val newEnd = runCatching {
+ val newEnd = runCatchingExceptions {
adjustLinkifiedUrlSpanEndIndex(spannable, start, end)
}.onFailure {
Timber.e(it, "Failed to adjust end index for link span")
diff --git a/libraries/architecture/build.gradle.kts b/libraries/architecture/build.gradle.kts
index 31c3d60757..8db06089f3 100644
--- a/libraries/architecture/build.gradle.kts
+++ b/libraries/architecture/build.gradle.kts
@@ -15,6 +15,7 @@ android {
dependencies {
api(projects.libraries.di)
+ api(projects.libraries.core)
api(libs.dagger)
api(libs.appyx.core)
api(libs.androidx.lifecycle.runtime)
diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncAction.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncAction.kt
index 6c4d286c2f..f080b64d01 100644
--- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncAction.kt
+++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncAction.kt
@@ -9,6 +9,7 @@ package io.element.android.libraries.architecture
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
@@ -90,7 +91,7 @@ suspend inline fun MutableState>.runCatchingUpdatingState(
state = this,
errorTransform = errorTransform,
resultBlock = {
- runCatching {
+ runCatchingExceptions {
block()
}
},
@@ -103,7 +104,7 @@ suspend inline fun (suspend () -> T).runCatchingUpdatingState(
state = state,
errorTransform = errorTransform,
resultBlock = {
- runCatching {
+ runCatchingExceptions {
this()
}
},
diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt
index c425597ba7..b7f22cbe23 100644
--- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt
+++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt
@@ -9,6 +9,7 @@ package io.element.android.libraries.architecture
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
@@ -93,7 +94,7 @@ suspend inline fun MutableState>.runCatchingUpdatingState(
state = this,
errorTransform = errorTransform,
resultBlock = {
- runCatching {
+ runCatchingExceptions {
block()
}
},
@@ -106,7 +107,7 @@ suspend inline fun (suspend () -> T).runCatchingUpdatingState(
state = state,
errorTransform = errorTransform,
resultBlock = {
- runCatching {
+ runCatchingExceptions {
this()
}
},
diff --git a/libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncDataKtTest.kt b/libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncDataKtTest.kt
index 1818e3e99e..1551360da2 100644
--- a/libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncDataKtTest.kt
+++ b/libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncDataKtTest.kt
@@ -38,15 +38,15 @@ class AsyncDataKtTest {
val result = runUpdatingState(state) {
delay(1)
- Result.failure(MyThrowable("hello"))
+ Result.failure(MyException("hello"))
}
assertThat(result.isFailure).isTrue()
- assertThat(result.exceptionOrNull()).isEqualTo(MyThrowable("hello"))
+ assertThat(result.exceptionOrNull()).isEqualTo(MyException("hello"))
assertThat(state.popFirst()).isEqualTo(AsyncData.Uninitialized)
assertThat(state.popFirst()).isEqualTo(AsyncData.Loading(null))
- assertThat(state.popFirst()).isEqualTo(AsyncData.Failure(MyThrowable("hello")))
+ assertThat(state.popFirst()).isEqualTo(AsyncData.Failure(MyException("hello")))
state.assertNoMoreValues()
}
@@ -54,17 +54,17 @@ class AsyncDataKtTest {
fun `updates state when block returns failure transforming the error`() = runTest {
val state = TestableMutableState>(AsyncData.Uninitialized)
- val result = runUpdatingState(state, { MyThrowable(it.message + " world") }) {
+ val result = runUpdatingState(state, { MyException(it.message + " world") }) {
delay(1)
- Result.failure(MyThrowable("hello"))
+ Result.failure(MyException("hello"))
}
assertThat(result.isFailure).isTrue()
- assertThat(result.exceptionOrNull()).isEqualTo(MyThrowable("hello world"))
+ assertThat(result.exceptionOrNull()).isEqualTo(MyException("hello world"))
assertThat(state.popFirst()).isEqualTo(AsyncData.Uninitialized)
assertThat(state.popFirst()).isEqualTo(AsyncData.Loading(null))
- assertThat(state.popFirst()).isEqualTo(AsyncData.Failure(MyThrowable("hello world")))
+ assertThat(state.popFirst()).isEqualTo(AsyncData.Failure(MyException("hello world")))
state.assertNoMoreValues()
}
}
@@ -101,4 +101,4 @@ private class TestableMutableState(
/**
* An exception that is also a data class so we can compare it using equals.
*/
-private data class MyThrowable(val myMessage: String) : Throwable(myMessage)
+private data class MyException(val myMessage: String) : Exception(myMessage)
diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/Try.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/Try.kt
index 222dcb8f30..0610fa04e4 100644
--- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/Try.kt
+++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/Try.kt
@@ -7,11 +7,20 @@
package io.element.android.libraries.core.data
-inline fun tryOrNull(onError: ((Throwable) -> Unit) = { }, operation: () -> A): A? {
+import kotlin.coroutines.cancellation.CancellationException
+
+/**
+ * Can be used to catch [Exception]s in a block of code, returning `null` if an exception occurs.
+ *
+ * If the block throws a [CancellationException], it will be rethrown.
+ */
+inline fun tryOrNull(onException: ((Exception) -> Unit) = { }, operation: () -> A): A? {
return try {
operation()
- } catch (any: Throwable) {
- onError.invoke(any)
+ } catch (e: CancellationException) {
+ throw e
+ } catch (e: Exception) {
+ onException.invoke(e)
null
}
}
diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt
index 3524a579b6..e46f37cffb 100644
--- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt
+++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt
@@ -7,6 +7,61 @@
package io.element.android.libraries.core.extensions
+import kotlin.coroutines.cancellation.CancellationException
+
+/**
+ * Can be used to catch exceptions in a block of code and return a [Result].
+ * If the block throws a [CancellationException], it will be rethrown.
+ * If it throws any other exception, it will be wrapped in a [Result.failure].
+ *
+ * [Error]s are not caught by this function, as they are not meant to be caught in normal application flow.
+ */
+inline fun runCatchingExceptions(
+ block: () -> T
+): Result {
+ return try {
+ Result.success(block())
+ } catch (e: CancellationException) {
+ throw e
+ } catch (e: Exception) {
+ Result.failure(e)
+ }
+}
+
+/**
+ * Can be used to catch exceptions in a block of code and return a [Result].
+ * If the block throws a [CancellationException], it will be rethrown.
+ * If it throws any other exception, it will be wrapped in a [Result.failure].
+ *
+ * [Error]s are not caught by this function, as they are not meant to be caught in normal application flow.
+ */
+inline fun T.runCatchingExceptions(
+ block: T.() -> R
+): Result {
+ return try {
+ Result.success(block())
+ } catch (e: CancellationException) {
+ throw e
+ } catch (e: Exception) {
+ Result.failure(e)
+ }
+}
+
+/**
+ * Can be used to transform a [Result] into another [Result] by applying a [block] to the value if it is successful.
+ * If the original [Result] is a failure, the exception will be wrapped in a new [Result.failure].
+ *
+ * This is a safer version of [Result.mapCatching].
+ */
+inline fun Result.mapCatchingExceptions(
+ block: (T) -> R,
+): Result {
+ return fold(
+ onSuccess = { value -> runCatchingExceptions { block(value) } },
+ onFailure = { exception -> Result.failure(exception) }
+ )
+}
+
/**
* Can be used to transform some Throwable into some other.
*/
@@ -33,12 +88,16 @@ inline fun Result.flatMap(transform: (T) -> Result): Result {
* @return The result of the transform or a caught exception wrapped in a [Result].
*/
inline fun Result.flatMapCatching(transform: (T) -> Result): Result {
- return mapCatching(transform).fold(
+ return mapCatchingExceptions(transform).fold(
onSuccess = { it },
onFailure = { Result.failure(it) }
)
}
+/**
+ * Can be used to execute a block of code after the [Result] has been processed, regardless of whether it was successful or not.
+ * The block receives the exception if there was one, or `null` if the result was successful.
+ */
inline fun Result.finally(block: (exception: Throwable?) -> Unit): Result {
onSuccess { block(null) }
onFailure(block)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt
index 39ec049be1..a2b0049ad6 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt
@@ -59,13 +59,11 @@ import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
@@ -92,9 +90,10 @@ import com.airbnb.android.showkase.annotation.ShowkaseComposable
import com.vanniktech.blurhash.BlurHash
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
-import io.element.android.libraries.designsystem.R
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
+import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
+import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -103,6 +102,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.utils.CommonDrawables
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
@@ -465,7 +465,9 @@ internal fun BloomPreview() {
var topAppBarHeight by remember { mutableIntStateOf(-1) }
val topAppBarState = rememberTopAppBarState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState)
- ElementPreview {
+ ElementPreview(
+ drawableFallbackForImages = CommonDrawables.sample_avatar,
+ ) {
Scaffold(
modifier = Modifier
.fillMaxSize()
@@ -489,14 +491,13 @@ internal fun BloomPreview() {
scrolledContainerColor = Color.Black.copy(alpha = 0.05f),
),
navigationIcon = {
- Image(
- modifier = Modifier
- .padding(start = 8.dp)
- .size(32.dp)
- .clip(CircleShape),
- painter = painterResource(id = R.drawable.sample_avatar),
- contentScale = ContentScale.Crop,
- contentDescription = null
+ Avatar(
+ avatarData = AvatarData(
+ id = "sample-avatar",
+ name = "sample",
+ url = "aURL",
+ size = AvatarSize.CurrentUserTopBar,
+ ),
)
},
actions = {
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt
index 6cfa911361..94ffd9c0fb 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt
@@ -21,7 +21,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.tooling.preview.Preview
@@ -29,7 +28,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter
import coil3.compose.SubcomposeAsyncImage
import coil3.compose.SubcomposeAsyncImageContent
@@ -37,9 +35,9 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
-import io.element.android.libraries.designsystem.preview.debugPlaceholderAvatar
import io.element.android.libraries.designsystem.text.toSp
import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.utils.CommonDrawables
import timber.log.Timber
@Composable
@@ -79,41 +77,30 @@ private fun ImageAvatar(
modifier: Modifier = Modifier,
contentDescription: String? = null,
) {
- if (LocalInspectionMode.current) {
- // For compose previews, use debugPlaceholderAvatar()
- // instead of falling back to initials avatar on load failure
- AsyncImage(
- model = avatarData,
- contentDescription = contentDescription,
- placeholder = debugPlaceholderAvatar(),
- modifier = modifier
- )
- } else {
- SubcomposeAsyncImage(
- model = avatarData,
- contentDescription = contentDescription,
- contentScale = ContentScale.Crop,
- modifier = modifier
- ) {
- val collectedState by painter.state.collectAsState()
- when (val state = collectedState) {
- is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent()
- is AsyncImagePainter.State.Error -> {
- SideEffect {
- Timber.e(state.result.throwable, "Error loading avatar $state\n${state.result}")
- }
- InitialsAvatar(
- avatarData = avatarData,
- forcedAvatarSize = forcedAvatarSize,
- contentDescription = contentDescription,
- )
+ SubcomposeAsyncImage(
+ model = avatarData,
+ contentDescription = contentDescription,
+ contentScale = ContentScale.Crop,
+ modifier = modifier
+ ) {
+ val collectedState by painter.state.collectAsState()
+ when (val state = collectedState) {
+ is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent()
+ is AsyncImagePainter.State.Error -> {
+ SideEffect {
+ Timber.e(state.result.throwable, "Error loading avatar $state\n${state.result}")
}
- else -> InitialsAvatar(
+ InitialsAvatar(
avatarData = avatarData,
forcedAvatarSize = forcedAvatarSize,
contentDescription = contentDescription,
)
}
+ else -> InitialsAvatar(
+ avatarData = avatarData,
+ forcedAvatarSize = forcedAvatarSize,
+ contentDescription = contentDescription,
+ )
}
}
}
@@ -151,7 +138,9 @@ private fun InitialsAvatar(
@Preview(group = PreviewGroup.Avatars)
@Composable
internal fun AvatarPreview(@PreviewParameter(AvatarDataProvider::class) avatarData: AvatarData) =
- ElementThemedPreview {
+ ElementThemedPreview(
+ drawableFallbackForImages = CommonDrawables.sample_avatar,
+ ) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Autofill.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Autofill.kt
deleted file mode 100644
index 5436a0330c..0000000000
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Autofill.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.libraries.designsystem.modifiers
-
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.AutofillNode
-import androidx.compose.ui.autofill.AutofillType
-import androidx.compose.ui.composed
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.layout.boundsInWindow
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalAutofill
-import androidx.compose.ui.platform.LocalAutofillTree
-
-@Suppress("ModifierComposed")
-@OptIn(ExperimentalComposeUiApi::class)
-fun Modifier.autofill(autofillTypes: List, onFill: (String) -> Unit) = composed {
- val autofillNode = AutofillNode(autofillTypes, onFill = onFill)
- LocalAutofillTree.current += autofillNode
-
- val autofill = LocalAutofill.current
-
- this
- .onGloballyPositioned {
- // Inform autofill framework of where our composable is so it can show the popup in the right place
- autofillNode.boundingBox = it.boundsInWindow()
- }
- .onFocusChanged {
- autofill?.run {
- if (it.isFocused) {
- requestAutofillForNode(autofillNode)
- } else {
- cancelAutofillForNode(autofillNode)
- }
- }
- }
-}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt
index 9a1548a687..788b8fee55 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt
@@ -7,14 +7,19 @@
package io.element.android.libraries.designsystem.preview
+import androidx.annotation.DrawableRes
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.core.content.res.ResourcesCompat
import coil3.annotation.ExperimentalCoilApi
+import coil3.asImage
import coil3.compose.AsyncImagePreviewHandler
import coil3.compose.LocalAsyncImagePreviewHandler
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Surface
+import io.element.android.libraries.designsystem.utils.CommonDrawables
@OptIn(ExperimentalCoilApi::class)
@Composable
@@ -22,9 +27,16 @@ import io.element.android.libraries.designsystem.theme.components.Surface
fun ElementPreview(
darkTheme: Boolean = isSystemInDarkTheme(),
showBackground: Boolean = true,
+ @DrawableRes
+ drawableFallbackForImages: Int = CommonDrawables.sample_background,
content: @Composable () -> Unit
) {
- CompositionLocalProvider(LocalAsyncImagePreviewHandler provides AsyncImagePreviewHandler { null }) {
+ val context = LocalContext.current
+ CompositionLocalProvider(
+ LocalAsyncImagePreviewHandler provides AsyncImagePreviewHandler {
+ ResourcesCompat.getDrawable(context.resources, drawableFallbackForImages, null)!!.asImage()
+ }
+ ) {
ElementTheme(darkTheme = darkTheme) {
if (showBackground) {
// If we have a proper contentColor applied we need a Surface instead of a Box
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementThemedPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementThemedPreview.kt
index 866735e69f..7ab7d323e3 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementThemedPreview.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementThemedPreview.kt
@@ -7,6 +7,7 @@
package io.element.android.libraries.designsystem.preview
+import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -19,12 +20,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.utils.CommonDrawables
@Composable
@Suppress("ModifierMissing")
fun ElementThemedPreview(
showBackground: Boolean = true,
vertical: Boolean = true,
+ @DrawableRes
+ drawableFallbackForImages: Int = CommonDrawables.sample_background,
content: @Composable () -> Unit,
) {
Box(
@@ -37,12 +41,14 @@ fun ElementThemedPreview(
ElementPreview(
darkTheme = false,
showBackground = showBackground,
+ drawableFallbackForImages = drawableFallbackForImages,
content = content,
)
Spacer(modifier = Modifier.height(4.dp))
ElementPreview(
darkTheme = true,
showBackground = showBackground,
+ drawableFallbackForImages = drawableFallbackForImages,
content = content
)
}
@@ -51,12 +57,14 @@ fun ElementThemedPreview(
ElementPreview(
darkTheme = false,
showBackground = showBackground,
+ drawableFallbackForImages = drawableFallbackForImages,
content = content,
)
Spacer(modifier = Modifier.width(4.dp))
ElementPreview(
darkTheme = true,
showBackground = showBackground,
+ drawableFallbackForImages = drawableFallbackForImages,
content = content
)
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/Images.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/Images.kt
deleted file mode 100644
index 74751deb06..0000000000
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/Images.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.libraries.designsystem.preview
-
-import androidx.annotation.DrawableRes
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.platform.LocalInspectionMode
-import androidx.compose.ui.res.painterResource
-import io.element.android.libraries.designsystem.R
-
-/**
- * I wanted to set up a FakeImageLoader as per https://github.com/coil-kt/coil/issues/1327
- * but it does not render in preview. In the meantime, you can use this trick to have image.
- */
-@Composable
-fun debugPlaceholder(
- @DrawableRes debugPreview: Int,
- nonDebugPainter: Painter? = null,
-) = if (LocalInspectionMode.current) {
- painterResource(id = debugPreview)
-} else {
- nonDebugPainter
-}
-
-@Composable
-fun debugPlaceholderBackground(nonDebugPainter: Painter? = null): Painter? {
- return debugPlaceholder(debugPreview = R.drawable.sample_background, nonDebugPainter)
-}
-
-@Composable
-fun debugPlaceholderAvatar(nonDebugPainter: Painter? = null): Painter? {
- return debugPlaceholder(debugPreview = R.drawable.sample_avatar, nonDebugPainter)
-}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt
index 5ac1dedb4a..d4db8923a0 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt
@@ -10,19 +10,18 @@ package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.BottomSheetDefaults
+import androidx.compose.material3.BottomSheetScaffoldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.contentColorFor
+import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
-import io.element.android.libraries.designsystem.theme.components.bottomsheet.BottomSheetScaffoldState
-import io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomBottomSheetScaffold
-import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberBottomSheetScaffoldState
@Composable
@ExperimentalMaterial3Api
@@ -44,7 +43,7 @@ fun BottomSheetScaffold(
contentColor: Color = contentColorFor(containerColor),
content: @Composable (PaddingValues) -> Unit
) {
- CustomBottomSheetScaffold(
+ androidx.compose.material3.BottomSheetScaffold(
sheetContent = sheetContent,
modifier = modifier,
scaffoldState = scaffoldState,
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt
index 51a6cd9ee1..e0d9c749ec 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
@@ -33,6 +34,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.preview.sheetStateForPreview
+import io.element.android.libraries.designsystem.utils.LocalUiTestMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -52,11 +54,14 @@ fun ModalBottomSheet(
content: @Composable ColumnScope.() -> Unit,
) {
val safeSheetState = if (LocalInspectionMode.current) sheetStateForPreview() else sheetState
+ // If we're running in UI test mode, we want to use a different shape to avoid
+ // this issue: https://issuetracker.google.com/issues/366255137
+ val safeShape = if (LocalUiTestMode.current) RoundedCornerShape(12.dp) else shape
androidx.compose.material3.ModalBottomSheet(
onDismissRequest = onDismissRequest,
modifier = modifier,
sheetState = safeSheetState,
- shape = shape,
+ shape = safeShape,
containerColor = containerColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt
deleted file mode 100644
index a819d9770b..0000000000
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt
+++ /dev/null
@@ -1,516 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-@file:OptIn(ExperimentalFoundationApi::class)
-
-package io.element.android.libraries.designsystem.theme.components.bottomsheet
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.DraggableAnchors
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.anchoredDraggable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.requiredHeightIn
-import androidx.compose.foundation.layout.widthIn
-import androidx.compose.material3.BottomSheetDefaults
-import androidx.compose.material3.BottomSheetScaffold
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.SheetState
-import androidx.compose.material3.SheetValue
-import androidx.compose.material3.SheetValue.Expanded
-import androidx.compose.material3.SheetValue.PartiallyExpanded
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.Surface
-import androidx.compose.material3.contentColorFor
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.layout.SubcomposeLayout
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.semantics.collapse
-import androidx.compose.ui.semantics.dismiss
-import androidx.compose.ui.semantics.expand
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.Velocity
-import kotlinx.coroutines.launch
-import kotlin.math.abs
-import kotlin.math.roundToInt
-
-// These are needed until https://issuetracker.google.com/issues/306464779 is fixed
-
-@Composable
-@ExperimentalMaterial3Api
-fun CustomBottomSheetScaffold(
- sheetContent: @Composable ColumnScope.() -> Unit,
- modifier: Modifier = Modifier,
- scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(),
- sheetPeekHeight: Dp = BottomSheetDefaults.SheetPeekHeight,
- sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
- sheetShape: Shape = BottomSheetDefaults.ExpandedShape,
- sheetContainerColor: Color = Color.White,
- sheetContentColor: Color = contentColorFor(sheetContainerColor),
- sheetTonalElevation: Dp = BottomSheetDefaults.Elevation,
- sheetShadowElevation: Dp = BottomSheetDefaults.Elevation,
- sheetDragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
- sheetSwipeEnabled: Boolean = true,
- topBar: @Composable (() -> Unit)? = null,
- snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
- containerColor: Color = MaterialTheme.colorScheme.surface,
- contentColor: Color = contentColorFor(containerColor),
- content: @Composable (PaddingValues) -> Unit
-) {
- val peekHeightPx = with(LocalDensity.current) {
- sheetPeekHeight.roundToPx()
- }
- CustomBottomSheetScaffoldLayout(
- modifier = modifier,
- topBar = topBar,
- body = content,
- snackbarHost = {
- snackbarHost(scaffoldState.snackbarHostState)
- },
- sheetPeekHeight = sheetPeekHeight,
- sheetOffset = { scaffoldState.bottomSheetState.requireOffset() },
- sheetState = scaffoldState.bottomSheetState,
- containerColor = containerColor,
- contentColor = contentColor,
- bottomSheet = { layoutHeight ->
- CustomStandardBottomSheet(
- state = scaffoldState.bottomSheetState,
- peekHeight = sheetPeekHeight,
- sheetMaxWidth = sheetMaxWidth,
- sheetSwipeEnabled = sheetSwipeEnabled,
- calculateAnchors = { sheetSize ->
- val sheetHeight = sheetSize.height
- io.element.android.libraries.designsystem.theme.components.bottomsheet.DraggableAnchors {
- if (!scaffoldState.bottomSheetState.skipPartiallyExpanded) {
- PartiallyExpanded at (layoutHeight - peekHeightPx).toFloat()
- }
- if (sheetHeight != peekHeightPx) {
- Expanded at maxOf(layoutHeight - sheetHeight, 0).toFloat()
- }
- if (!scaffoldState.bottomSheetState.skipHiddenState) {
- SheetValue.Hidden at layoutHeight.toFloat()
- }
- }
- },
- shape = sheetShape,
- containerColor = sheetContainerColor,
- contentColor = sheetContentColor,
- tonalElevation = sheetTonalElevation,
- shadowElevation = sheetShadowElevation,
- dragHandle = sheetDragHandle,
- content = sheetContent
- )
- }
- )
-}
-
-@SuppressWarnings("ModifierWithoutDefault")
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun CustomBottomSheetScaffoldLayout(
- modifier: Modifier,
- topBar: @Composable (() -> Unit)?,
- body: @Composable (innerPadding: PaddingValues) -> Unit,
- bottomSheet: @Composable (layoutHeight: Int) -> Unit,
- snackbarHost: @Composable () -> Unit,
- sheetPeekHeight: Dp,
- sheetOffset: () -> Float,
- sheetState: CustomSheetState,
- containerColor: Color,
- contentColor: Color,
-) {
- // b/291735717 Remove this once deprecated methods without density are removed
- val density = LocalDensity.current
- SideEffect {
- sheetState.density = density
- }
- SubcomposeLayout { constraints ->
- val layoutWidth = constraints.maxWidth
- val layoutHeight = constraints.maxHeight
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
-
- val sheetPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Sheet) {
- bottomSheet(layoutHeight)
- }[0].measure(looseConstraints)
-
- val topBarPlaceable = topBar?.let {
- subcompose(BottomSheetScaffoldLayoutSlot.TopBar) { topBar() }[0]
- .measure(looseConstraints)
- }
- val topBarHeight = topBarPlaceable?.height ?: 0
-
- val bodyConstraints = looseConstraints.copy(maxHeight = layoutHeight - topBarHeight)
- val bodyPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Body) {
- Surface(
- modifier = modifier,
- color = containerColor,
- contentColor = contentColor,
- ) { body(PaddingValues(bottom = sheetPeekHeight)) }
- }[0].measure(bodyConstraints)
-
- val snackbarPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Snackbar, snackbarHost)[0]
- .measure(looseConstraints)
-
- layout(layoutWidth, layoutHeight) {
- val sheetOffsetY = sheetOffset().roundToInt()
- val sheetOffsetX = Integer.max(0, (layoutWidth - sheetPlaceable.width) / 2)
-
- val snackbarOffsetX = (layoutWidth - snackbarPlaceable.width) / 2
- val snackbarOffsetY = when (sheetState.currentValue) {
- SheetValue.PartiallyExpanded -> sheetOffsetY - snackbarPlaceable.height
- SheetValue.Expanded, SheetValue.Hidden -> layoutHeight - snackbarPlaceable.height
- }
-
- // Placement order is important for elevation
- bodyPlaceable.placeRelative(0, topBarHeight)
- topBarPlaceable?.placeRelative(0, 0)
- sheetPlaceable.placeRelative(sheetOffsetX, sheetOffsetY)
- snackbarPlaceable.placeRelative(snackbarOffsetX, snackbarOffsetY)
- }
- }
-}
-
-private enum class BottomSheetScaffoldLayoutSlot { TopBar, Body, Sheet, Snackbar }
-
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
-@Composable
-private fun CustomStandardBottomSheet(
- state: CustomSheetState,
- @Suppress("PrimitiveInLambda")
- calculateAnchors: (sheetSize: IntSize) -> DraggableAnchors,
- peekHeight: Dp,
- sheetMaxWidth: Dp,
- sheetSwipeEnabled: Boolean,
- shape: Shape,
- containerColor: Color,
- contentColor: Color,
- tonalElevation: Dp,
- shadowElevation: Dp,
- dragHandle: @Composable (() -> Unit)?,
- content: @Composable ColumnScope.() -> Unit
-) {
- val scope = rememberCoroutineScope()
-
- val orientation = Orientation.Vertical
-
- Surface(
- modifier = Modifier
- .widthIn(max = sheetMaxWidth)
- .fillMaxWidth()
- .requiredHeightIn(min = peekHeight)
- .apply {
- if (sheetSwipeEnabled) {
- nestedScroll(
- remember(state.anchoredDraggableState) {
- ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
- sheetState = state,
- orientation = orientation,
- onFling = { scope.launch { state.settle(it) } }
- )
- }
- )
- }
- }
- .anchoredDraggable(
- state = state.anchoredDraggableState,
- orientation = orientation,
- enabled = sheetSwipeEnabled
- )
- .onSizeChanged { layoutSize ->
- val newAnchors = calculateAnchors(layoutSize)
- val newTarget = when (state.anchoredDraggableState.targetValue) {
- SheetValue.Hidden, SheetValue.PartiallyExpanded -> SheetValue.PartiallyExpanded
- SheetValue.Expanded -> {
- if (newAnchors.hasAnchorFor(SheetValue.Expanded)) SheetValue.Expanded else SheetValue.PartiallyExpanded
- }
- }
- state.anchoredDraggableState.updateAnchors(newAnchors, newTarget)
- },
- shape = shape,
- color = containerColor,
- contentColor = contentColor,
- tonalElevation = tonalElevation,
- shadowElevation = shadowElevation,
- ) {
- Column(Modifier.fillMaxWidth()) {
- if (dragHandle != null) {
- val partialExpandActionLabel =
- "Partial Expand"
- val dismissActionLabel = "Dismiss"
- val expandActionLabel = "Expand"
- Box(
- Modifier
- .align(Alignment.CenterHorizontally)
- .semantics(mergeDescendants = true) {
- with(state) {
- // Provides semantics to interact with the bottomsheet if there is more
- // than one anchor to swipe to and swiping is enabled.
- if (anchoredDraggableState.anchors.size > 1 && sheetSwipeEnabled) {
- if (currentValue == SheetValue.PartiallyExpanded) {
- expand(expandActionLabel) {
- scope.launch { expand() }
- true
- }
- } else {
- collapse(partialExpandActionLabel) {
- scope.launch { partialExpand() }
- true
- }
- }
- if (!state.skipHiddenState) {
- dismiss(dismissActionLabel) {
- scope.launch { hide() }
- true
- }
- }
- }
- }
- },
- ) {
- dragHandle()
- }
- }
- content()
- }
- }
-}
-
-/**
- * [DraggableAnchorsConfig] stores a mutable configuration anchors, comprised of values of [T] and
- * corresponding [Float] positions. This [DraggableAnchorsConfig] is used to construct an immutable
- * [DraggableAnchors] instance later on.
- */
-@ExperimentalFoundationApi
-class DraggableAnchorsConfig {
- internal val anchors = mutableMapOf()
-
- /**
- * Set the anchor position for [this] anchor.
- *
- * @param position The anchor position.
- */
- @Suppress("BuilderSetStyle")
- infix fun T.at(position: Float) {
- anchors[this] = position
- }
-}
-
-/**
- * Create a new [DraggableAnchors] instance using a builder function.
- *
- * @param T The type of the anchor values.
- * @param builder A function with a [DraggableAnchorsConfig] that offers APIs to configure anchors
- * @return A new [DraggableAnchors] instance with the anchor positions set by the `builder`
- * function.
- */
-@OptIn(ExperimentalFoundationApi::class)
-@ExperimentalMaterial3Api
-@SuppressWarnings("FunctionName")
-internal fun DraggableAnchors(
- builder: DraggableAnchorsConfig.() -> Unit
-): DraggableAnchors = MapDraggableAnchors(DraggableAnchorsConfig().apply(builder).anchors)
-
-private class MapDraggableAnchors(private val anchors: Map) : DraggableAnchors {
- override fun positionOf(value: T): Float = anchors[value] ?: Float.NaN
- override fun hasAnchorFor(value: T) = anchors.containsKey(value)
-
- override fun closestAnchor(position: Float): T? = anchors.minByOrNull {
- abs(position - it.value)
- }?.key
-
- override fun closestAnchor(
- position: Float,
- searchUpwards: Boolean
- ): T? {
- return anchors.minByOrNull { (_, anchor) ->
- val delta = if (searchUpwards) anchor - position else position - anchor
- if (delta < 0) Float.POSITIVE_INFINITY else delta
- }?.key
- }
-
- override fun minAnchor() = anchors.values.minOrNull() ?: Float.NaN
-
- override fun maxAnchor() = anchors.values.maxOrNull() ?: Float.NaN
-
- override val size: Int
- get() = anchors.size
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is MapDraggableAnchors<*>) return false
-
- return anchors == other.anchors
- }
-
- override fun forEach(block: (anchor: T, position: Float) -> Unit) {
- for (anchor in anchors) {
- block(anchor.key, anchor.value)
- }
- }
-
- override fun hashCode() = 31 * anchors.hashCode()
-
- override fun toString() = "MapDraggableAnchors($anchors)"
-}
-
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
-@SuppressWarnings("FunctionName")
-internal fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
- sheetState: CustomSheetState,
- orientation: Orientation,
- onFling: (velocity: Float) -> Unit
-): NestedScrollConnection = object : NestedScrollConnection {
- override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
- val delta = available.toFloat()
- return if (delta < 0 && source == NestedScrollSource.UserInput) {
- sheetState.anchoredDraggableState.dispatchRawDelta(delta).toOffset()
- } else {
- Offset.Zero
- }
- }
-
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- return if (source == NestedScrollSource.UserInput) {
- sheetState.anchoredDraggableState.dispatchRawDelta(available.toFloat()).toOffset()
- } else {
- Offset.Zero
- }
- }
-
- override suspend fun onPreFling(available: Velocity): Velocity {
- val toFling = available.toFloat()
- val currentOffset = sheetState.requireOffset()
- val minAnchor = sheetState.anchoredDraggableState.anchors.minAnchor()
- return if (toFling < 0 && currentOffset > minAnchor) {
- onFling(toFling)
- // since we go to the anchor with tween settling, consume all for the best UX
- available
- } else {
- Velocity.Zero
- }
- }
-
- override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
- onFling(available.toFloat())
- return available
- }
-
- private fun Float.toOffset(): Offset = Offset(
- x = if (orientation == Orientation.Horizontal) this else 0f,
- y = if (orientation == Orientation.Vertical) this else 0f
- )
-
- @JvmName("velocityToFloat")
- private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y
-
- @JvmName("offsetToFloat")
- private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y
-}
-
-/**
- * State of the [BottomSheetScaffold] composable.
- *
- * @param bottomSheetState the state of the persistent bottom sheet
- * @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold
- */
-@ExperimentalMaterial3Api
-@Stable
-@SuppressWarnings("UseDataClass")
-class BottomSheetScaffoldState(
- val bottomSheetState: CustomSheetState,
- val snackbarHostState: SnackbarHostState
-)
-
-/**
- * Create and [remember] a [BottomSheetScaffoldState].
- *
- * @param bottomSheetState the state of the standard bottom sheet. See
- * [rememberStandardBottomSheetState]
- * @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold
- */
-@Composable
-@ExperimentalMaterial3Api
-fun rememberBottomSheetScaffoldState(
- bottomSheetState: CustomSheetState = rememberStandardBottomSheetState(),
- snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
-): BottomSheetScaffoldState {
- return remember(bottomSheetState, snackbarHostState) {
- BottomSheetScaffoldState(
- bottomSheetState = bottomSheetState,
- snackbarHostState = snackbarHostState
- )
- }
-}
-
-/**
- * Create and [remember] a [SheetState] for [BottomSheetScaffold].
- *
- * @param initialValue the initial value of the state. Should be either [PartiallyExpanded] or
- * [Expanded] if [skipHiddenState] is true
- * @param confirmValueChange optional callback invoked to confirm or veto a pending state change
- * @param [skipHiddenState] whether Hidden state is skipped for [BottomSheetScaffold]
- */
-@Composable
-@ExperimentalMaterial3Api
-fun rememberStandardBottomSheetState(
- initialValue: SheetValue = PartiallyExpanded,
- confirmValueChange: (SheetValue) -> Boolean = { true },
- skipHiddenState: Boolean = true,
-) = rememberSheetState(false, confirmValueChange, initialValue, skipHiddenState)
-
-@Composable
-@ExperimentalMaterial3Api
-internal fun rememberSheetState(
- skipPartiallyExpanded: Boolean = false,
- confirmValueChange: (SheetValue) -> Boolean = { true },
- initialValue: SheetValue = SheetValue.Hidden,
- skipHiddenState: Boolean = false,
-): CustomSheetState {
- val density = LocalDensity.current
- return rememberSaveable(
- skipPartiallyExpanded,
- confirmValueChange,
- saver = CustomSheetState.Saver(
- skipPartiallyExpanded = skipPartiallyExpanded,
- confirmValueChange = confirmValueChange,
- density = density
- )
- ) {
- CustomSheetState(
- skipPartiallyExpanded,
- density,
- initialValue,
- confirmValueChange,
- skipHiddenState
- )
- }
-}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt
deleted file mode 100644
index 85507485e3..0000000000
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.libraries.designsystem.theme.components.bottomsheet
-
-import androidx.compose.animation.core.SpringSpec
-import androidx.compose.animation.core.exponentialDecay
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.animateTo
-import androidx.compose.foundation.gestures.snapTo
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.SheetValue
-import androidx.compose.material3.SheetValue.Expanded
-import androidx.compose.material3.SheetValue.Hidden
-import androidx.compose.material3.SheetValue.PartiallyExpanded
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.saveable.Saver
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.CancellationException
-
-@OptIn(ExperimentalFoundationApi::class)
-@Stable
-@ExperimentalMaterial3Api
-class CustomSheetState
-@Deprecated(
- message = "This constructor is deprecated. " +
- "Please use the constructor that provides a [Density]",
- replaceWith = ReplaceWith(
- "SheetState(" +
- "skipPartiallyExpanded, LocalDensity.current, initialValue, " +
- "confirmValueChange, skipHiddenState)"
- )
-)
-constructor(
- internal val skipPartiallyExpanded: Boolean,
- initialValue: SheetValue = Hidden,
- confirmValueChange: (SheetValue) -> Boolean = { true },
- internal val skipHiddenState: Boolean = false,
-) {
- /**
- * State of a sheet composable, such as [ModalBottomSheet]
- *
- * Contains states relating to its swipe position as well as animations between state values.
- *
- * @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large
- * enough, should be skipped. If true, the sheet will always expand to the [Expanded] state and move
- * to the [Hidden] state if available when hiding the sheet, either programmatically or by user
- * interaction.
- * @param density The density that this state can use to convert values to and from dp.
- * @param initialValue The initial value of the state.
- * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
- * @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always
- * expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either
- * programmatically or by user interaction.
- */
- @ExperimentalMaterial3Api
- @Suppress("Deprecation")
- constructor(
- skipPartiallyExpanded: Boolean,
- density: Density,
- initialValue: SheetValue = Hidden,
- confirmValueChange: (SheetValue) -> Boolean = { true },
- skipHiddenState: Boolean = false,
- ) : this(skipPartiallyExpanded, initialValue, confirmValueChange, skipHiddenState) {
- this.density = density
- }
-
- init {
- if (skipPartiallyExpanded) {
- require(initialValue != PartiallyExpanded) {
- "The initial value must not be set to PartiallyExpanded if skipPartiallyExpanded " +
- "is set to true."
- }
- }
- if (skipHiddenState) {
- require(initialValue != Hidden) {
- "The initial value must not be set to Hidden if skipHiddenState is set to true."
- }
- }
- }
-
- /**
- * The current value of the state.
- *
- * If no swipe or animation is in progress, this corresponds to the state the bottom sheet is
- * currently in. If a swipe or an animation is in progress, this corresponds the state the sheet
- * was in before the swipe or animation started.
- */
-
- val currentValue: SheetValue get() = anchoredDraggableState.currentValue
-
- /**
- * The target value of the bottom sheet state.
- *
- * If a swipe is in progress, this is the value that the sheet would animate to if the
- * swipe finishes. If an animation is running, this is the target value of that animation.
- * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
- */
- val targetValue: SheetValue get() = anchoredDraggableState.targetValue
-
- /**
- * Whether the modal bottom sheet is visible.
- */
- val isVisible: Boolean
- get() = anchoredDraggableState.currentValue != Hidden
-
- /**
- * Require the current offset (in pixels) of the bottom sheet.
- *
- * The offset will be initialized during the first measurement phase of the provided sheet
- * content.
- *
- * These are the phases:
- * Composition { -> Effects } -> Layout { Measurement -> Placement } -> Drawing
- *
- * During the first composition, an [IllegalStateException] is thrown. In subsequent
- * compositions, the offset will be derived from the anchors of the previous pass. Always prefer
- * accessing the offset from a LaunchedEffect as it will be scheduled to be executed the next
- * frame, after layout.
- *
- * @throws IllegalStateException If the offset has not been initialized yet
- */
- fun requireOffset(): Float = anchoredDraggableState.requireOffset()
-
- fun getOffset(): Float? = anchoredDraggableState.offset.takeIf { !it.isNaN() }
-
- /**
- * Whether the sheet has an expanded state defined.
- */
-
- val hasExpandedState: Boolean
- get() = anchoredDraggableState.anchors.hasAnchorFor(Expanded)
-
- /**
- * Whether the modal bottom sheet has a partially expanded state defined.
- */
- val hasPartiallyExpandedState: Boolean
- get() = anchoredDraggableState.anchors.hasAnchorFor(PartiallyExpanded)
-
- /**
- * Fully expand the bottom sheet with animation and suspend until it is fully expanded or
- * animation has been cancelled.
- * *
- * @throws [CancellationException] if the animation is interrupted
- */
- suspend fun expand() {
- anchoredDraggableState.animateTo(Expanded)
- }
-
- /**
- * Animate the bottom sheet and suspend until it is partially expanded or animation has been
- * cancelled.
- * @throws [CancellationException] if the animation is interrupted
- * @throws [IllegalStateException] if [skipPartiallyExpanded] is set to true
- */
- suspend fun partialExpand() {
- check(!skipPartiallyExpanded) {
- "Attempted to animate to partial expanded when skipPartiallyExpanded was enabled. Set" +
- " skipPartiallyExpanded to false to use this function."
- }
- animateTo(PartiallyExpanded)
- }
-
- /**
- * Expand the bottom sheet with animation and suspend until it is [PartiallyExpanded] if defined
- * else [Expanded].
- * @throws [CancellationException] if the animation is interrupted
- */
- suspend fun show() {
- val targetValue = when {
- hasPartiallyExpandedState -> PartiallyExpanded
- else -> Expanded
- }
- animateTo(targetValue)
- }
-
- /**
- * Hide the bottom sheet with animation and suspend until it is fully hidden or animation has
- * been cancelled.
- * @throws [CancellationException] if the animation is interrupted
- */
- suspend fun hide() {
- check(!skipHiddenState) {
- "Attempted to animate to hidden when skipHiddenState was enabled. Set skipHiddenState" +
- " to false to use this function."
- }
- animateTo(Hidden)
- }
-
- /**
- * Animate to a [targetValue].
- * If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the
- * [targetValue] without updating the offset.
- *
- * @throws CancellationException if the interaction interrupted by another interaction like a
- * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
- *
- * @param targetValue The target value of the animation
- */
- @OptIn(ExperimentalFoundationApi::class)
- internal suspend fun animateTo(
- targetValue: SheetValue,
- ) {
- anchoredDraggableState.animateTo(targetValue)
- }
-
- /**
- * Snap to a [targetValue] without any animation.
- *
- * @throws CancellationException if the interaction interrupted by another interaction like a
- * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
- *
- * @param targetValue The target value of the animation
- */
- @OptIn(ExperimentalFoundationApi::class)
- internal suspend fun snapTo(targetValue: SheetValue) {
- anchoredDraggableState.snapTo(targetValue)
- }
-
- /**
- * Find the closest anchor taking into account the velocity and settle at it with an animation.
- */
- @OptIn(ExperimentalFoundationApi::class)
- internal suspend fun settle(velocity: Float) {
- anchoredDraggableState.settle(velocity)
- }
-
- @OptIn(ExperimentalFoundationApi::class)
- internal var anchoredDraggableState = androidx.compose.foundation.gestures.AnchoredDraggableState(
- initialValue = initialValue,
- snapAnimationSpec = AnchoredDraggableDefaults.SnapAnimationSpec,
- decayAnimationSpec = AnchoredDraggableDefaults.DecayAnimationSpec,
- confirmValueChange = confirmValueChange,
- positionalThreshold = { with(requireDensity()) { 56.dp.toPx() } },
- velocityThreshold = { with(requireDensity()) { 125.dp.toPx() } }
- )
-
- @OptIn(ExperimentalFoundationApi::class)
- internal val offset: Float? get() = anchoredDraggableState.offset
-
- internal var density: Density? = null
- private fun requireDensity() = requireNotNull(density) {
- "SheetState did not have a density attached. Are you using SheetState with " +
- "BottomSheetScaffold or ModalBottomSheet component?"
- }
-
- companion object {
- /**
- * The default [Saver] implementation for [SheetState].
- */
- @SuppressWarnings("FunctionName")
- fun Saver(
- skipPartiallyExpanded: Boolean,
- confirmValueChange: (SheetValue) -> Boolean,
- density: Density
- ) = Saver(
- save = { it.currentValue },
- restore = { savedValue ->
- CustomSheetState(skipPartiallyExpanded, density, savedValue, confirmValueChange)
- }
- )
-
- /**
- * The default [Saver] implementation for [SheetState].
- */
- @Deprecated(
- message = "This function is deprecated. Please use the overload where Density is" +
- " provided.",
- replaceWith = ReplaceWith(
- "Saver(skipPartiallyExpanded, confirmValueChange, LocalDensity.current)"
- )
- )
- @Suppress("Deprecation", "FunctionName")
- fun Saver(
- skipPartiallyExpanded: Boolean,
- confirmValueChange: (SheetValue) -> Boolean
- ) = Saver(
- save = { it.currentValue },
- restore = { savedValue ->
- CustomSheetState(skipPartiallyExpanded, savedValue, confirmValueChange)
- }
- )
- }
-}
-
-@Stable
-@ExperimentalMaterial3Api
-internal object AnchoredDraggableDefaults {
- /**
- * The default animation used by [AnchoredDraggableState].
- */
- @ExperimentalMaterial3Api
- val SnapAnimationSpec = SpringSpec()
-
- @ExperimentalMaterial3Api
- val DecayAnimationSpec = exponentialDecay()
-}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LocalUiTestMode.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LocalUiTestMode.kt
new file mode 100644
index 0000000000..5c1288f0e7
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LocalUiTestMode.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.designsystem.utils
+
+import androidx.compose.runtime.staticCompositionLocalOf
+
+/**
+ * A composition local that indicates whether the app is running in UI test mode.
+ */
+val LocalUiTestMode = staticCompositionLocalOf { false }
diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/AppCoroutineScope.kt b/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/AppCoroutineScope.kt
new file mode 100644
index 0000000000..ea597e56e1
--- /dev/null
+++ b/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/AppCoroutineScope.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.di.annotations
+
+import javax.inject.Qualifier
+
+/**
+ * Qualifies a [CoroutineScope] object which represents the base coroutine scope to use for the application.
+ */
+@Retention(AnnotationRetention.RUNTIME)
+@MustBeDocumented
+@Qualifier
+annotation class AppCoroutineScope
diff --git a/libraries/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 56d3bfa1e2..c0005b7cfa 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
@@ -31,7 +31,6 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
-import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
@@ -41,8 +40,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
import java.util.Optional
interface MatrixClient {
@@ -55,7 +52,7 @@ interface MatrixClient {
val ignoredUsersFlow: StateFlow>
suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom?
suspend fun getRoom(roomId: RoomId): BaseRoom?
- suspend fun findDM(userId: UserId): RoomId?
+ suspend fun findDM(userId: UserId): Result
suspend fun ignoreUser(userId: UserId): Result
suspend fun unignoreUser(userId: UserId): Result
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result
@@ -65,9 +62,9 @@ interface MatrixClient {
suspend fun setDisplayName(displayName: String): Result
suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result
suspend fun removeAvatar(): Result
- suspend fun joinRoom(roomId: RoomId): Result
- suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result
- suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result
+ suspend fun joinRoom(roomId: RoomId): Result
+ suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result
+ suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result
fun syncService(): SyncService
fun sessionVerificationService(): SessionVerificationService
fun pushersService(): PushersService
@@ -99,11 +96,11 @@ interface MatrixClient {
fun roomMembershipObserver(): RoomMembershipObserver
/**
- * Get a room summary flow for a given room ID or alias.
- * The flow will emit a new value whenever the room summary is updated.
+ * Get a room info flow for a given room ID.
+ * The flow will emit a new value whenever the room info is updated.
* The flow will emit Optional.empty item if the room is not found.
*/
- fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias): Flow>
+ fun getRoomInfoFlow(roomId: RoomId): Flow>
fun isMe(userId: UserId?) = userId == sessionId
@@ -169,17 +166,6 @@ interface MatrixClient {
suspend fun canReportRoom(): Boolean
}
-/**
- * Get a room info flow for a given room ID or alias.
- * The flow will emit a new value whenever the room info is updated.
- * The flow will emit Optional.empty item if the room is not found.
- */
-fun MatrixClient.getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow> {
- return getRoomSummaryFlow(roomIdOrAlias)
- .map { roomSummary -> roomSummary.map { it.info } }
- .distinctUntilChanged()
-}
-
/**
* Returns a room alias from a room alias name, or null if the name is not valid.
* @param name the room alias name ie. the local part of the room alias.
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StartDM.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StartDM.kt
index 7755d97d8c..f9b0de2449 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StartDM.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StartDM.kt
@@ -18,17 +18,24 @@ suspend fun MatrixClient.startDM(
userId: UserId,
createIfDmDoesNotExist: Boolean,
): StartDMResult {
- val existingDM = findDM(userId)
- return if (existingDM != null) {
- StartDMResult.Success(existingDM, isNew = false)
- } else if (createIfDmDoesNotExist) {
- createDM(userId).fold(
- { StartDMResult.Success(it, isNew = true) },
- { StartDMResult.Failure(it) }
+ return findDM(userId)
+ .fold(
+ onSuccess = { existingDM ->
+ if (existingDM != null) {
+ StartDMResult.Success(existingDM, isNew = false)
+ } else if (createIfDmDoesNotExist) {
+ createDM(userId).fold(
+ { StartDMResult.Success(it, isNew = true) },
+ { StartDMResult.Failure(it) }
+ )
+ } else {
+ StartDMResult.DmDoesNotExist
+ }
+ },
+ onFailure = { error ->
+ StartDMResult.Failure(error)
+ }
)
- } else {
- StartDMResult.DmDoesNotExist
- }
}
sealed interface StartDMResult {
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevels.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevels.kt
index 95cbf58e17..8f548cc601 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevels.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevels.kt
@@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.api.room.powerlevels
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType
@@ -60,7 +61,7 @@ suspend fun BaseRoom.canRedactOther(): Result = canUserRedactOther(sess
/**
* Shortcut for checking if current user can handle knock requests.
*/
-suspend fun BaseRoom.canHandleKnockRequests(): Result = runCatching {
+suspend fun BaseRoom.canHandleKnockRequests(): Result = runCatchingExceptions {
canInvite().getOrThrow() || canBan().getOrThrow() || canKick().getOrThrow()
}
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 816e4f26bd..007628b8e1 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
@@ -13,6 +13,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.extensions.mapFailure
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.DeviceId
@@ -21,7 +22,6 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId
-import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.createroom.RoomPreset
import io.element.android.libraries.matrix.api.encryption.EncryptionService
@@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.NotJoinedRoom
+import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
@@ -41,7 +42,6 @@ import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.roomlist.RoomListService
-import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
@@ -59,6 +59,7 @@ import io.element.android.libraries.matrix.impl.pushers.RustPushersService
import io.element.android.libraries.matrix.impl.room.GetRoomResult
import io.element.android.libraries.matrix.impl.room.NotJoinedRustRoom
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
+import io.element.android.libraries.matrix.impl.room.RoomInfoMapper
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
import io.element.android.libraries.matrix.impl.room.RustRoomFactory
import io.element.android.libraries.matrix.impl.room.TimelineEventTypeFilterFactory
@@ -69,6 +70,7 @@ import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryS
import io.element.android.libraries.matrix.impl.roomdirectory.map
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
+import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.matrix.impl.sync.map
import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
@@ -92,7 +94,6 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
@@ -108,6 +109,7 @@ import org.matrix.rustcomponents.sdk.ClientException
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
import org.matrix.rustcomponents.sdk.PowerLevels
+import org.matrix.rustcomponents.sdk.RoomInfoListener
import org.matrix.rustcomponents.sdk.SendQueueRoomErrorListener
import org.matrix.rustcomponents.sdk.TaskHandle
import org.matrix.rustcomponents.sdk.use
@@ -187,6 +189,7 @@ class RustMatrixClient(
sessionCoroutineScope = sessionCoroutineScope,
)
+ private val roomInfoMapper = RoomInfoMapper()
private val roomMembershipObserver = RoomMembershipObserver()
private val roomFactory = RustRoomFactory(
roomListService = roomListService,
@@ -202,6 +205,7 @@ class RustMatrixClient(
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
featureFlagService = featureFlagService,
roomMembershipObserver = roomMembershipObserver,
+ roomInfoMapper = roomInfoMapper,
)
override val mediaLoader: MatrixMediaLoader = RustMediaLoader(
@@ -250,7 +254,7 @@ class RustMatrixClient(
}
override fun userIdServerName(): String {
- return runCatching {
+ return runCatchingExceptions {
innerClient.userIdServerName()
}
.onFailure {
@@ -261,7 +265,7 @@ class RustMatrixClient(
}
override suspend fun getUrl(url: String): Result = withContext(sessionDispatcher) {
- runCatching {
+ runCatchingExceptions {
innerClient.getUrl(url)
}
}
@@ -275,45 +279,46 @@ class RustMatrixClient(
}
/**
- * Wait for the room to be available in the room list with the correct membership for the current user.
- * @param roomIdOrAlias the room id or alias to wait for
+ * Wait for the room to be available in the client with the correct membership for the current user.
+ * @param roomId the room id to wait for
* @param timeout the timeout to wait for the room to be available
* @param currentUserMembership the membership to wait for
* @throws TimeoutCancellationException if the room is not available after the timeout
*/
private suspend fun awaitRoom(
- roomIdOrAlias: RoomIdOrAlias,
+ roomId: RoomId,
timeout: Duration,
currentUserMembership: CurrentUserMembership,
- ): RoomSummary {
+ ): RoomInfo {
return withTimeout(timeout) {
- getRoomSummaryFlow(roomIdOrAlias)
- .mapNotNull { optionalRoomSummary -> optionalRoomSummary.getOrNull() }
- .filter { roomSummary -> roomSummary.info.currentUserMembership == currentUserMembership }
- .first()
+ getRoomInfoFlow(roomId)
+ .mapNotNull { roomInfo -> roomInfo.getOrNull() }
+ .first { info -> info.currentUserMembership == currentUserMembership }
// Ensure that the room is ready
- .also { innerClient.awaitRoomRemoteEcho(it.roomId.value) }
+ .also { innerClient.awaitRoomRemoteEcho(roomId.value).destroy() }
}
}
- override suspend fun findDM(userId: UserId): RoomId? = withContext(sessionDispatcher) {
- innerClient.getDmRoom(userId.value)?.use { RoomId(it.id()) }
+ override suspend fun findDM(userId: UserId): Result = withContext(sessionDispatcher) {
+ runCatchingExceptions {
+ innerClient.getDmRoom(userId.value)?.use { RoomId(it.id()) }
+ }
}
override suspend fun ignoreUser(userId: UserId): Result = withContext(sessionDispatcher) {
- runCatching {
+ runCatchingExceptions {
innerClient.ignoreUser(userId.value)
}
}
override suspend fun unignoreUser(userId: UserId): Result = withContext(sessionDispatcher) {
- runCatching {
+ runCatchingExceptions {
innerClient.unignoreUser(userId.value)
}
}
override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result = withContext(sessionDispatcher) {
- runCatching {
+ runCatchingExceptions {
val rustParams = RustCreateRoomParameters(
name = createRoomParams.name,
topic = createRoomParams.topic,
@@ -342,7 +347,7 @@ class RustMatrixClient(
val roomId = RoomId(innerClient.createRoom(rustParams))
// Wait to receive the room back from the sync but do not returns failure if it fails.
try {
- awaitRoom(roomId.toRoomIdOrAlias(), 30.seconds, CurrentUserMembership.JOINED)
+ awaitRoom(roomId, 30.seconds, CurrentUserMembership.JOINED)
} catch (e: Exception) {
Timber.e(e, "Timeout waiting for the room to be available in the room list")
}
@@ -363,7 +368,7 @@ class RustMatrixClient(
}
override suspend fun getProfile(userId: UserId): Result = withContext(sessionDispatcher) {
- runCatching {
+ runCatchingExceptions {
innerClient.getProfile(userId.value).let(UserProfileMapper::map)
}
}
@@ -373,31 +378,31 @@ class RustMatrixClient(
override suspend fun searchUsers(searchTerm: String, limit: Long): Result =
withContext(sessionDispatcher) {
- runCatching {
+ runCatchingExceptions {
innerClient.searchUsers(searchTerm, limit.toULong()).let(UserSearchResultMapper::map)
}
}
override suspend fun setDisplayName(displayName: String): Result =
withContext(sessionDispatcher) {
- runCatching { innerClient.setDisplayName(displayName) }
+ runCatchingExceptions { innerClient.setDisplayName(displayName) }
}
override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result =
withContext(sessionDispatcher) {
- runCatching { innerClient.uploadAvatar(mimeType, data) }
+ runCatchingExceptions { innerClient.uploadAvatar(mimeType, data) }
}
override suspend fun removeAvatar(): Result =
withContext(sessionDispatcher) {
- runCatching { innerClient.removeAvatar() }
+ runCatchingExceptions { innerClient.removeAvatar() }
}
- override suspend fun joinRoom(roomId: RoomId): Result = withContext(sessionDispatcher) {
- runCatching {
+ override suspend fun joinRoom(roomId: RoomId): Result = withContext(sessionDispatcher) {
+ runCatchingExceptions {
innerClient.joinRoomById(roomId.value).destroy()
try {
- awaitRoom(roomId.toRoomIdOrAlias(), 10.seconds, CurrentUserMembership.JOINED)
+ awaitRoom(roomId, 10.seconds, CurrentUserMembership.JOINED)
} catch (e: Exception) {
Timber.e(e, "Timeout waiting for the room to be available in the room list")
null
@@ -405,14 +410,16 @@ class RustMatrixClient(
}
}.mapFailure { it.mapClientException() }
- override suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result = withContext(sessionDispatcher) {
- runCatching {
- innerClient.joinRoomByIdOrAlias(
+ override suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result = withContext(sessionDispatcher) {
+ runCatchingExceptions {
+ val roomId = innerClient.joinRoomByIdOrAlias(
roomIdOrAlias = roomIdOrAlias.identifier,
serverNames = serverNames,
- ).destroy()
+ ).use {
+ RoomId(it.id())
+ }
try {
- awaitRoom(roomIdOrAlias, 10.seconds, CurrentUserMembership.JOINED)
+ awaitRoom(roomId, 10.seconds, CurrentUserMembership.JOINED)
} catch (e: Exception) {
Timber.e(e, "Timeout waiting for the room to be available in the room list")
null
@@ -420,13 +427,15 @@ class RustMatrixClient(
}.mapFailure { it.mapClientException() }
}
- override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result = withContext(
+ override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result = withContext(
sessionDispatcher
) {
- runCatching {
- innerClient.knock(roomIdOrAlias.identifier, message, serverNames).destroy()
+ runCatchingExceptions {
+ val roomId = innerClient.knock(roomIdOrAlias.identifier, message, serverNames).use {
+ RoomId(it.id())
+ }
try {
- awaitRoom(roomIdOrAlias, 10.seconds, CurrentUserMembership.KNOCKED)
+ awaitRoom(roomId, 10.seconds, CurrentUserMembership.KNOCKED)
} catch (e: Exception) {
Timber.e(e, "Timeout waiting for the room to be available in the room list")
null
@@ -435,19 +444,19 @@ class RustMatrixClient(
}
override suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result = withContext(sessionDispatcher) {
- runCatching {
+ runCatchingExceptions {
innerClient.trackRecentlyVisitedRoom(roomId.value)
}
}
override suspend fun getRecentlyVisitedRooms(): Result> = withContext(sessionDispatcher) {
- runCatching {
+ runCatchingExceptions {
innerClient.getRecentlyVisitedRooms().map(::RoomId)
}
}
override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result> = withContext(sessionDispatcher) {
- runCatching {
+ runCatchingExceptions {
val result = innerClient.resolveRoomAlias(roomAlias.value)?.let {
ResolvedRoomAlias(
roomId = RoomId(it.roomId),
@@ -459,7 +468,7 @@ class RustMatrixClient(
}
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result = withContext(sessionDispatcher) {
- runCatching {
+ runCatchingExceptions {
when (roomIdOrAlias) {
is RoomIdOrAlias.Alias -> {
val roomId = innerClient.resolveRoomAlias(roomIdOrAlias.roomAlias.value)?.roomId?.let { RoomId(it) }
@@ -556,7 +565,7 @@ class RustMatrixClient(
}
override fun canDeactivateAccount(): Boolean {
- return runCatching {
+ return runCatchingExceptions {
innerClient.canDeactivateAccount()
}
.getOrNull()
@@ -568,9 +577,9 @@ class RustMatrixClient(
// Remove current delegate so we don't receive an auth error
clientDelegateTaskHandle?.cancelAndDestroy()
clientDelegateTaskHandle = null
- runCatching {
+ runCatchingExceptions {
// First call without AuthData, should fail
- val firstAttempt = runCatching {
+ val firstAttempt = runCatchingExceptions {
innerClient.deactivateAccount(
authData = null,
eraseData = eraseData,
@@ -579,7 +588,7 @@ class RustMatrixClient(
if (firstAttempt.isFailure) {
Timber.w(firstAttempt.exceptionOrNull(), "Expected failure, try again")
// This is expected, try again with the password
- runCatching {
+ runCatchingExceptions {
innerClient.deactivateAccount(
authData = AuthData.Password(
passwordDetails = AuthDataPasswordDetails(
@@ -606,34 +615,32 @@ class RustMatrixClient(
override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result = withContext(sessionDispatcher) {
val rustAction = action?.toRustAction()
- runCatching {
+ runCatchingExceptions {
innerClient.accountUrl(rustAction)
}
}
override suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result = withContext(sessionDispatcher) {
- runCatching {
+ runCatchingExceptions {
innerClient.uploadMedia(mimeType, data, progressCallback?.toProgressWatcher())
}
}
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
- override fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias): Flow> {
- val predicate: (RoomSummary) -> Boolean = when (roomIdOrAlias) {
- is RoomIdOrAlias.Alias -> { roomSummary ->
- roomSummary.info.aliases.contains(roomIdOrAlias.roomAlias)
+ override fun getRoomInfoFlow(roomId: RoomId): Flow> {
+ return mxCallbackFlow {
+ val roomNotFound = innerRoomListService.roomOrNull(roomId.value).use { it == null }
+ if (roomNotFound) {
+ channel.send(Optional.empty())
}
- is RoomIdOrAlias.Id -> { roomSummary ->
- roomSummary.roomId == roomIdOrAlias.roomId
- }
- }
- return roomListService.allRooms.summaries
- .map { roomSummaries ->
- val roomSummary = roomSummaries.firstOrNull(predicate)
- Optional.ofNullable(roomSummary)
- }
- .distinctUntilChanged()
+ innerClient.subscribeToRoomInfo(roomId.value, object : RoomInfoListener {
+ override fun call(roomInfo: org.matrix.rustcomponents.sdk.RoomInfo) {
+ val mappedRoomInfo = roomInfoMapper.map(roomInfo)
+ channel.trySend(Optional.of(mappedRoomInfo))
+ }
+ })
+ }.distinctUntilChanged()
}
override suspend fun setAllSendQueuesEnabled(enabled: Boolean) {
@@ -654,19 +661,19 @@ class RustMatrixClient(
}.buffer(Channel.UNLIMITED)
override suspend fun availableSlidingSyncVersions(): Result> = withContext(sessionDispatcher) {
- runCatching {
+ runCatchingExceptions {
innerClient.availableSlidingSyncVersions().map { it.map() }
}
}
override suspend fun currentSlidingSyncVersion(): Result = withContext(sessionDispatcher) {
- runCatching {
+ runCatchingExceptions {
innerClient.session().slidingSyncVersion.map()
}
}
override suspend fun canReportRoom(): Boolean = withContext(sessionDispatcher) {
- runCatching {
+ runCatchingExceptions {
innerClient.isReportRoomApiSupported()
}.getOrDefault(false)
}
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 a88c9bab14..bc8e337ada 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
@@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.CacheDirectory
+import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.impl.analytics.UtdTracker
@@ -40,6 +41,7 @@ import javax.inject.Inject
class RustMatrixClientFactory @Inject constructor(
private val baseDirectory: File,
@CacheDirectory private val cacheDirectory: File,
+ @AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers,
private val sessionStore: SessionStore,
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt
index 2770dc1250..66fcee1e48 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt
@@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl.analytics
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
+import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.isDm
import kotlinx.coroutines.flow.first
@@ -26,10 +27,14 @@ private fun Long.toAnalyticsRoomSize(): JoinedRoom.RoomSize {
suspend fun BaseRoom.toAnalyticsJoinedRoom(trigger: JoinedRoom.Trigger?): JoinedRoom {
val roomInfo = roomInfoFlow.first()
+ return roomInfo.toAnalyticsJoinedRoom(trigger)
+}
+
+fun RoomInfo.toAnalyticsJoinedRoom(trigger: JoinedRoom.Trigger?): JoinedRoom {
return JoinedRoom(
- isDM = roomInfo.isDm,
- isSpace = roomInfo.isSpace,
- roomSize = roomInfo.joinedMembersCount.toAnalyticsRoomSize(),
+ isDM = isDm,
+ isSpace = isSpace,
+ roomSize = joinedMembersCount.toAnalyticsRoomSize(),
trigger = trigger
)
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
index 8f7135a914..506abcf5da 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
@@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.impl.auth
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient
@@ -91,7 +92,7 @@ class RustMatrixAuthenticationService @Inject constructor(
}
override suspend fun restoreSession(sessionId: SessionId): Result = withContext(coroutineDispatchers.io) {
- runCatching {
+ runCatchingExceptions {
val sessionData = sessionStore.getSession(sessionId.value)
if (sessionData != null) {
if (sessionData.isTokenValid) {
@@ -126,7 +127,7 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun setHomeserver(homeserver: String): Result =
withContext(coroutineDispatchers.io) {
val emptySessionPath = rotateSessionPath()
- runCatching {
+ runCatchingExceptions {
val client = makeClient(sessionPaths = emptySessionPath) {
serverNameOrHomeserverUrl(homeserver)
}
@@ -144,7 +145,7 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun login(username: String, password: String): Result =
withContext(coroutineDispatchers.io) {
- runCatching {
+ runCatchingExceptions {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
client.login(username, password, "Element X Android", null)
@@ -170,7 +171,7 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun importCreatedSession(externalSession: ExternalSession): Result =
withContext(coroutineDispatchers.io) {
- runCatching {
+ runCatchingExceptions {
currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
val sessionData = externalSession.toSessionData(
@@ -192,7 +193,7 @@ class RustMatrixAuthenticationService @Inject constructor(
loginHint: String?,
): Result {
return withContext(coroutineDispatchers.io) {
- runCatching {
+ runCatchingExceptions {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val oAuthAuthorizationData = client.urlForOidc(
oidcConfiguration = oidcConfigurationProvider.get(),
@@ -211,7 +212,7 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun cancelOidcLogin(): Result {
return withContext(coroutineDispatchers.io) {
- runCatching {
+ runCatchingExceptions {
pendingOAuthAuthorizationData?.use {
currentClient?.abortOidcAuth(it)
}
@@ -228,7 +229,7 @@ class RustMatrixAuthenticationService @Inject constructor(
*/
override suspend fun loginWithOidc(callbackUrl: String): Result {
return withContext(coroutineDispatchers.io) {
- runCatching {
+ runCatchingExceptions {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
client.loginWithOidcCallback(callbackUrl)
@@ -268,7 +269,7 @@ class RustMatrixAuthenticationService @Inject constructor(
progress(state.toStep())
}
}
- runCatching {
+ runCatchingExceptions {
val client = makeQrCodeLoginClient(
sessionPaths = emptySessionPaths,
passphrase = pendingPassphrase,
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/RustQrCodeLoginDataFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/RustQrCodeLoginDataFactory.kt
index 7efb918bfa..367a272a75 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/RustQrCodeLoginDataFactory.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/RustQrCodeLoginDataFactory.kt
@@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.impl.auth.qrlogin
import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDataFactory
@@ -17,6 +18,6 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class)
class RustQrCodeLoginDataFactory @Inject constructor() : MatrixQrCodeLoginDataFactory {
override fun parseQrCodeData(data: ByteArray): Result {
- return runCatching { SdkQrCodeLoginData(QrCodeData.fromBytes(data)) }
+ return runCatchingExceptions { SdkQrCodeLoginData(QrCodeData.fromBytes(data)) }
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/ElementWellKnownParser.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/ElementWellKnownParser.kt
index 46d601f523..710e18f5ac 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/ElementWellKnownParser.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/ElementWellKnownParser.kt
@@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.impl.call
import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.AppScope
import org.matrix.rustcomponents.sdk.ElementWellKnown
import org.matrix.rustcomponents.sdk.makeElementWellKnown
@@ -20,7 +21,7 @@ interface ElementWellKnownParser {
@ContributesBinding(AppScope::class)
class RustElementWellKnownParser @Inject constructor() : ElementWellKnownParser {
override fun parse(str: String): Result {
- return runCatching {
+ return runCatchingExceptions {
makeElementWellKnown(str)
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/core/RustSendHandle.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/core/RustSendHandle.kt
index 41a6edf240..9d5e113703 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/core/RustSendHandle.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/core/RustSendHandle.kt
@@ -7,13 +7,14 @@
package io.element.android.libraries.matrix.impl.core
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.SendHandle
class RustSendHandle(
val inner: org.matrix.rustcomponents.sdk.SendHandle,
) : SendHandle {
override suspend fun retry(): Result {
- return runCatching {
+ return runCatchingExceptions {
inner.tryResend()
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
index 0ffe5208f7..7c87666fe7 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
@@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.impl.encryption
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.extensions.mapFailure
+import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.BackupState
@@ -96,7 +97,7 @@ internal class RustEncryptionService(
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
override suspend fun enableBackups(): Result = withContext(dispatchers.io) {
- runCatching {
+ runCatchingExceptions {
service.enableBackups()
}.mapFailure {
it.mapRecoveryException()
@@ -106,7 +107,7 @@ internal class RustEncryptionService(
override suspend fun enableRecovery(
waitForBackupsToUpload: Boolean,
): Result = withContext(dispatchers.io) {
- runCatching {
+ runCatchingExceptions {
service.enableRecovery(
waitForBackupsToUpload = waitForBackupsToUpload,
progressListener = object : EnableRecoveryProgressListener {
@@ -124,14 +125,14 @@ internal class RustEncryptionService(
}
override suspend fun doesBackupExistOnServer(): Result = withContext(dispatchers.io) {
- runCatching {
+ runCatchingExceptions {
service.backupExistsOnServer()
}
}
override fun waitForBackupUploadSteadyState(): Flow {
return callbackFlow {
- runCatching {
+ runCatchingExceptions {
service.waitForBackupUploadSteadyState(
progressListener = object : BackupSteadyStateListener {
override fun onUpdate(status: RustBackupUploadState) {
@@ -155,7 +156,7 @@ internal class RustEncryptionService(
}
override suspend fun disableRecovery(): Result = withContext(dispatchers.io) {
- runCatching {
+ runCatchingExceptions {
service.disableRecovery()
}.mapFailure {
it.mapRecoveryException()
@@ -163,7 +164,7 @@ internal class RustEncryptionService(
}
private suspend fun isLastDevice(): Result = withContext(dispatchers.io) {
- runCatching {
+ runCatchingExceptions {
service.isLastDevice()
}.mapFailure {
it.mapRecoveryException()
@@ -171,7 +172,7 @@ internal class RustEncryptionService(
}
override suspend fun resetRecoveryKey(): Result = withContext(dispatchers.io) {
- runCatching {
+ runCatchingExceptions {
service.resetRecoveryKey()
}.mapFailure {
it.mapRecoveryException()
@@ -179,7 +180,7 @@ internal class RustEncryptionService(
}
override suspend fun recover(recoveryKey: String): Result = withContext(dispatchers.io) {
- runCatching {
+ runCatchingExceptions {
service.recover(recoveryKey)
}.mapFailure {
it.mapRecoveryException()
@@ -187,34 +188,34 @@ internal class RustEncryptionService(
}
override suspend fun deviceCurve25519(): String? {
- return runCatching { service.curve25519Key() }.getOrNull()
+ return runCatchingExceptions { service.curve25519Key() }.getOrNull()
}
override suspend fun deviceEd25519(): String? {
- return runCatching { service.ed25519Key() }.getOrNull()
+ return runCatchingExceptions { service.ed25519Key() }.getOrNull()
}
override suspend fun startIdentityReset(): Result {
- return runCatching {
+ return runCatchingExceptions {
service.resetIdentity()
}.flatMap { handle ->
RustIdentityResetHandleFactory.create(sessionId, handle)
}
}
- override suspend fun isUserVerified(userId: UserId): Result = runCatching {
+ override suspend fun isUserVerified(userId: UserId): Result = runCatchingExceptions {
getUserIdentityInternal(userId).isVerified()
}
- override suspend fun pinUserIdentity(userId: UserId): Result